From cacc6fba6a13273e3e64de3de09a197b6fa436a8 Mon Sep 17 00:00:00 2001 From: Jan Jansen Date: Wed, 3 Apr 2019 21:18:01 +0200 Subject: [PATCH 1/3] Modernize Documentation using mkdocs #1420 * Automate travis ci build and release * Use mkdocs-material as design for the documentation * Convert all asciidocs into markdown Part of #1420 Signed-off-by: Jan Jansen --- .gitignore | 2 + .remarkrc.yml | 2 + .travis.yml | 236 +- BUILDING.md | 16 +- CONTRIBUTING.md | 4 +- RELEASING.md | 36 +- docs.Dockerfile | 17 + docs/adoc_attributes.adoc | 3 - docs/advanced-topics/advschema.md | 154 + .../bigtablemodel.png | Bin docs/advanced-topics/bulk-loading.md | 205 + docs/advanced-topics/data-model.md | 104 + docs/advanced-topics/eventual-consistency.md | 197 + .../hadoop.md} | 179 +- docs/advanced-topics/index-admin.md | 500 + .../janusgraph-bus.md} | 7 +- docs/advanced-topics/migrating.md | 63 + docs/advanced-topics/monitoring.md | 404 + docs/advanced-topics/partitioning.md | 108 + docs/advanced-topics/recovery.md | 103 + .../relationlayout.png | Bin docs/advanced-topics/serializer.md | 49 + .../storagelayout.png | Bin docs/advanced.adoc | 24 - docs/advblueprints.adoc | 28 - docs/advschema.adoc | 93 - docs/appendices.adoc | 20 - docs/appendices.md | 8 + .../architecture-layer-diagram.svg | 0 docs/basics.adoc | 1613 --- .../images => basics}/advanced-scenario.svg | 0 .../images => basics}/advanced-scenario.xml | 0 docs/basics/cache.md | 166 + docs/basics/common-questions.md | 152 + docs/basics/configuration-reference.md | 67 + docs/basics/configuration.md | 246 + .../configured-graph-factory.md} | 394 +- docs/basics/deployment.md | 92 + docs/basics/example-config.md | 100 + .../getting-started-scenario.svg | 0 .../getting-started-scenario.xml | 0 docs/basics/gremlin.md | 195 + docs/basics/index-performance.md | 488 + docs/basics/janusgraph-cfg.md | 470 + .../images => basics}/minimalist-scenario.svg | 0 .../images => basics}/minimalist-scenario.xml | 0 docs/basics/multi-node.md | 170 + docs/basics/schema.md | 265 + docs/basics/server.md | 469 + docs/basics/technical-limitations.md | 104 + docs/basics/transaction-log.md | 194 + docs/basics/transactions.md | 324 + docs/bdb.adoc | 44 - docs/bigtable.adoc | 35 - docs/build-and-copy-docs.sh | 87 - docs/building.adoc | 43 - docs/bulkloading.adoc | 103 - docs/{static/images => }/cassandra-small.svg | 0 docs/cassandra.adoc | 232 - docs/changelog.adoc | 164 - docs/changelog.md | 369 + docs/configref.adoc | 38 - docs/connecting-via-dotnet.adoc | 51 - docs/connecting-via-java.adoc | 88 - docs/connecting-via-python.adoc | 52 - docs/connecting.adoc | 16 - docs/connecting/dotnet.md | 49 + docs/connecting/index.md | 12 + docs/connecting/java.md | 80 + docs/connecting/python.md | 53 + docs/datamodel.adoc | 45 - docs/deploymentscenarios.adoc | 45 - docs/development.adoc | 91 - docs/development.md | 215 + docs/directindex.adoc | 80 - docs/doc-versions.adoc | 14 - docs/elasticsearch.adoc | 159 - docs/eventualconsistency.adoc | 109 - docs/exampleconfig.adoc | 75 - docs/favicon.ico | Bin 0 -> 1371 bytes docs/generating.adoc | 41 - .../images => }/graph-of-the-gods-2.png | Bin docs/hbase.adoc | 204 - docs/images/logos/celum.png | Bin 0 -> 10562 bytes docs/images/logos/compose.png | Bin 0 -> 57915 bytes docs/images/logos/finc.png | Bin 0 -> 20682 bytes docs/images/logos/gdata.png | Bin 0 -> 18332 bytes docs/images/logos/qihoo_360.png | Bin 0 -> 5941 bytes docs/images/logos/redhat.png | Bin 0 -> 39062 bytes docs/images/logos/timesinternet.png | Bin 0 -> 16411 bytes docs/index-backend/direct-index-query.md | 150 + docs/index-backend/elasticsearch.md | 263 + docs/index-backend/field-mapping.md | 85 + docs/index-backend/index.md | 5 + docs/index-backend/lucene.md | 70 + .../search-predicates.md} | 71 +- docs/index-backend/solr.md | 555 + docs/index-backend/text-search.md | 165 + docs/index.adoc | 21 - docs/index.md | 457 + docs/indexbackends.adoc | 21 - docs/inmemorybackend.adoc | 21 - docs/internals.adoc | 6 - docs/intro.adoc | 339 - docs/janusgraph-logomark.svg | 1 + docs/janusgraph.png | Bin 0 -> 14442 bytes docs/listings/janusgraph_cfg.adoc | 1 - docs/lucene.adoc | 40 - docs/migrating.adoc | 35 - docs/monitoring.adoc | 217 - docs/partitioning.adoc | 53 - docs/recovery.adoc | 51 - docs/reindex.adoc | 380 - docs/serializer.adoc | 27 - docs/solr.adoc | 441 - docs/static/css/docs.css | 397 - docs/static/favicon.ico | Bin 24838 -> 0 bytes docs/static/images/icons/README | 5 - docs/static/images/icons/callouts/1.png | Bin 329 -> 0 bytes docs/static/images/icons/callouts/10.png | Bin 361 -> 0 bytes docs/static/images/icons/callouts/11.png | Bin 565 -> 0 bytes docs/static/images/icons/callouts/12.png | Bin 617 -> 0 bytes docs/static/images/icons/callouts/13.png | Bin 623 -> 0 bytes docs/static/images/icons/callouts/14.png | Bin 411 -> 0 bytes docs/static/images/icons/callouts/15.png | Bin 640 -> 0 bytes docs/static/images/icons/callouts/2.png | Bin 353 -> 0 bytes docs/static/images/icons/callouts/3.png | Bin 350 -> 0 bytes docs/static/images/icons/callouts/4.png | Bin 345 -> 0 bytes docs/static/images/icons/callouts/5.png | Bin 348 -> 0 bytes docs/static/images/icons/callouts/6.png | Bin 355 -> 0 bytes docs/static/images/icons/callouts/7.png | Bin 344 -> 0 bytes docs/static/images/icons/callouts/8.png | Bin 357 -> 0 bytes docs/static/images/icons/callouts/9.png | Bin 357 -> 0 bytes docs/static/images/icons/caution.png | Bin 2734 -> 0 bytes docs/static/images/icons/example.png | Bin 2599 -> 0 bytes docs/static/images/icons/home.png | Bin 1340 -> 0 bytes docs/static/images/icons/important.png | Bin 2980 -> 0 bytes docs/static/images/icons/next.png | Bin 1302 -> 0 bytes docs/static/images/icons/note.png | Bin 2494 -> 0 bytes docs/static/images/icons/prev.png | Bin 1348 -> 0 bytes docs/static/images/icons/tip.png | Bin 2718 -> 0 bytes docs/static/images/icons/up.png | Bin 1320 -> 0 bytes docs/static/images/icons/warning.png | Bin 3214 -> 0 bytes docs/static/images/janusgraph-logo.png | Bin 4199 -> 0 bytes docs/static/images/janusgraph-logomark.png | Bin 1944 -> 0 bytes docs/static/images/splash-graph.png | Bin 270041 -> 0 bytes docs/static/js/jquery/jquery-1.11.0.js | 5 - .../js/jquery/jquery-migrate-1.2.1.min.js | 2 - docs/storage-backend/bdb.md | 89 + docs/storage-backend/bigtable.md | 37 + docs/storage-backend/cassandra.md | 407 + .../cloud-bigtable.svg} | 0 docs/storage-backend/hbase.md | 279 + docs/storage-backend/index.md | 9 + docs/storage-backend/inmemorybackend.md | 33 + .../modes-distributed.png | Bin .../modes-embedded.png | Bin .../modes-local.png | Bin .../modes-rexster.png | Bin docs/storagebackends.adoc | 22 - docs/textsearch.adoc | 167 - docs/theme/extra.css | 15 + docs/theme/structor-menu.css | 10 + docs/theme/structor-menu.js.gotmpl | 125 + docs/tutorials.adoc | 0 docs/upgrade.adoc | 114 - docs/usefultools.adoc | 6 - docs/versions.adoc | 26 - docs/xsl/chunk.xsl | 100 - docs/xsl/common.xsl | 173 - docs/xsl/docbook/VERSION.xsl | 115 - docs/xsl/docbook/common/addns.xsl | 124 - docs/xsl/docbook/common/af.xml | 1271 -- docs/xsl/docbook/common/am.xml | 1271 -- docs/xsl/docbook/common/ar.xml | 1271 -- docs/xsl/docbook/common/as.xml | 702 - docs/xsl/docbook/common/ast.xml | 702 - docs/xsl/docbook/common/autoidx-kimber.xsl | 45 - docs/xsl/docbook/common/autoidx-kosek.xsl | 155 - docs/xsl/docbook/common/az.xml | 714 - docs/xsl/docbook/common/bg.xml | 766 -- docs/xsl/docbook/common/bn.xml | 1271 -- docs/xsl/docbook/common/bn_in.xml | 702 - docs/xsl/docbook/common/bs.xml | 704 - docs/xsl/docbook/common/ca.xml | 702 - docs/xsl/docbook/common/charmap.xml | 185 - docs/xsl/docbook/common/charmap.xsl | 222 - docs/xsl/docbook/common/common.xml | 641 - docs/xsl/docbook/common/common.xsl | 2111 --- docs/xsl/docbook/common/cs.xml | 742 - docs/xsl/docbook/common/cy.xml | 1287 -- docs/xsl/docbook/common/da.xml | 706 - docs/xsl/docbook/common/de.xml | 708 - docs/xsl/docbook/common/el.xml | 771 -- docs/xsl/docbook/common/en.xml | 1271 -- docs/xsl/docbook/common/entities.ent | 67 - docs/xsl/docbook/common/eo.xml | 1271 -- docs/xsl/docbook/common/es.xml | 718 - docs/xsl/docbook/common/et.xml | 1271 -- docs/xsl/docbook/common/eu.xml | 1271 -- docs/xsl/docbook/common/fa.xml | 702 - docs/xsl/docbook/common/fi.xml | 712 - docs/xsl/docbook/common/fr.xml | 732 - docs/xsl/docbook/common/ga.xml | 1271 -- docs/xsl/docbook/common/gentext.xsl | 847 -- docs/xsl/docbook/common/gl.xml | 1271 -- docs/xsl/docbook/common/gu.xml | 702 - docs/xsl/docbook/common/he.xml | 1271 -- docs/xsl/docbook/common/hi.xml | 702 - docs/xsl/docbook/common/hr.xml | 704 - docs/xsl/docbook/common/hu.xml | 720 - docs/xsl/docbook/common/id.xml | 1271 -- docs/xsl/docbook/common/insertfile.xsl | 113 - docs/xsl/docbook/common/is.xml | 714 - docs/xsl/docbook/common/it.xml | 1271 -- docs/xsl/docbook/common/ja.xml | 702 - docs/xsl/docbook/common/ka.xml | 742 - docs/xsl/docbook/common/kn.xml | 1271 -- docs/xsl/docbook/common/ko.xml | 1271 -- docs/xsl/docbook/common/ky.xml | 774 -- docs/xsl/docbook/common/l10n.dtd | 64 - docs/xsl/docbook/common/l10n.xml | 77 - docs/xsl/docbook/common/l10n.xsl | 598 - docs/xsl/docbook/common/la.xml | 1271 -- docs/xsl/docbook/common/labels.xsl | 893 -- docs/xsl/docbook/common/lt.xml | 720 - docs/xsl/docbook/common/lv.xml | 1271 -- docs/xsl/docbook/common/ml.xml | 702 - docs/xsl/docbook/common/mn.xml | 772 -- docs/xsl/docbook/common/mr.xml | 702 - docs/xsl/docbook/common/nb.xml | 1271 -- docs/xsl/docbook/common/nds.xml | 708 - docs/xsl/docbook/common/nl.xml | 702 - docs/xsl/docbook/common/nn.xml | 1271 -- docs/xsl/docbook/common/olink.xsl | 1240 -- docs/xsl/docbook/common/or.xml | 1271 -- docs/xsl/docbook/common/pa.xml | 702 - docs/xsl/docbook/common/pi.xml | 168 - docs/xsl/docbook/common/pi.xsl | 347 - docs/xsl/docbook/common/pl.xml | 720 - docs/xsl/docbook/common/pt.xml | 1271 -- docs/xsl/docbook/common/pt_br.xml | 1271 -- docs/xsl/docbook/common/refentry.xml | 781 -- docs/xsl/docbook/common/refentry.xsl | 1353 -- docs/xsl/docbook/common/ro.xml | 1271 -- docs/xsl/docbook/common/ru.xml | 768 -- docs/xsl/docbook/common/sk.xml | 1271 -- docs/xsl/docbook/common/sl.xml | 1271 -- docs/xsl/docbook/common/sq.xml | 1271 -- docs/xsl/docbook/common/sr.xml | 762 -- docs/xsl/docbook/common/sr_Latn.xml | 721 - docs/xsl/docbook/common/subtitles.xsl | 181 - docs/xsl/docbook/common/sv.xml | 706 - docs/xsl/docbook/common/ta.xml | 702 - docs/xsl/docbook/common/table.xsl | 515 - docs/xsl/docbook/common/targetdatabase.dtd | 49 - docs/xsl/docbook/common/targets.xsl | 338 - docs/xsl/docbook/common/te.xml | 702 - docs/xsl/docbook/common/th.xml | 1271 -- docs/xsl/docbook/common/titles.xsl | 822 -- docs/xsl/docbook/common/tl.xml | 1271 -- docs/xsl/docbook/common/tr.xml | 708 - docs/xsl/docbook/common/uk.xml | 768 -- docs/xsl/docbook/common/utility.xml | 259 - docs/xsl/docbook/common/utility.xsl | 291 - docs/xsl/docbook/common/vi.xml | 1271 -- docs/xsl/docbook/common/xh.xml | 1271 -- docs/xsl/docbook/common/zh.xml | 702 - docs/xsl/docbook/common/zh_cn.xml | 702 - docs/xsl/docbook/common/zh_tw.xml | 702 - docs/xsl/docbook/highlighting/bourne-hl.xml | 95 - docs/xsl/docbook/highlighting/c-hl.xml | 117 - docs/xsl/docbook/highlighting/cmake-hl.xml | 187 - docs/xsl/docbook/highlighting/common.xsl | 121 - docs/xsl/docbook/highlighting/cpp-hl.xml | 151 - docs/xsl/docbook/highlighting/csharp-hl.xml | 194 - docs/xsl/docbook/highlighting/css21-hl.xml | 176 - docs/xsl/docbook/highlighting/delphi-hl.xml | 220 - docs/xsl/docbook/highlighting/ini-hl.xml | 45 - docs/xsl/docbook/highlighting/java-hl.xml | 117 - .../docbook/highlighting/javascript-hl.xml | 147 - docs/xsl/docbook/highlighting/lua-hl.xml | 140 - docs/xsl/docbook/highlighting/m2-hl.xml | 90 - docs/xsl/docbook/highlighting/myxml-hl.xml | 116 - docs/xsl/docbook/highlighting/perl-hl.xml | 120 - docs/xsl/docbook/highlighting/php-hl.xml | 154 - docs/xsl/docbook/highlighting/python-hl.xml | 100 - docs/xsl/docbook/highlighting/ruby-hl.xml | 109 - docs/xsl/docbook/highlighting/sql1999-hl.xml | 496 - docs/xsl/docbook/highlighting/sql2003-hl.xml | 565 - docs/xsl/docbook/highlighting/sql92-hl.xml | 339 - docs/xsl/docbook/highlighting/tcl-hl.xml | 180 - docs/xsl/docbook/highlighting/upc-hl.xml | 133 - .../docbook/highlighting/xslthl-config.xml | 56 - docs/xsl/docbook/html/admon.xsl | 141 - docs/xsl/docbook/html/annotations.xsl | 171 - docs/xsl/docbook/html/autoidx-kimber.xsl | 166 - docs/xsl/docbook/html/autoidx-kosek.xsl | 121 - docs/xsl/docbook/html/autoidx-ng.xsl | 22 - docs/xsl/docbook/html/autoidx.xsl | 798 -- docs/xsl/docbook/html/autotoc.xsl | 758 -- docs/xsl/docbook/html/biblio-iso690.xsl | 1302 -- docs/xsl/docbook/html/biblio.xsl | 1384 -- docs/xsl/docbook/html/block.xsl | 585 - docs/xsl/docbook/html/callout.xsl | 223 - docs/xsl/docbook/html/changebars.xsl | 124 - docs/xsl/docbook/html/chunk-changebars.xsl | 100 - docs/xsl/docbook/html/chunk-code.xsl | 698 - docs/xsl/docbook/html/chunk-common.xsl | 2005 --- docs/xsl/docbook/html/chunk.xsl | 53 - docs/xsl/docbook/html/chunker.xsl | 453 - docs/xsl/docbook/html/chunkfast.xsl | 73 - docs/xsl/docbook/html/chunktoc.xsl | 548 - docs/xsl/docbook/html/component.xsl | 472 - docs/xsl/docbook/html/division.xsl | 214 - docs/xsl/docbook/html/docbook.css.xml | 110 - docs/xsl/docbook/html/docbook.xsl | 563 - docs/xsl/docbook/html/ebnf.xsl | 332 - docs/xsl/docbook/html/footnote.xsl | 358 - docs/xsl/docbook/html/formal.xsl | 513 - docs/xsl/docbook/html/glossary.xsl | 530 - docs/xsl/docbook/html/graphics.xsl | 1608 --- docs/xsl/docbook/html/highlight.xsl | 85 - docs/xsl/docbook/html/html-rtf.xsl | 336 - docs/xsl/docbook/html/html.xsl | 700 - docs/xsl/docbook/html/htmltbl.xsl | 135 - docs/xsl/docbook/html/index.xsl | 281 - docs/xsl/docbook/html/info.xsl | 47 - docs/xsl/docbook/html/inline.xsl | 1533 --- docs/xsl/docbook/html/keywords.xsl | 37 - docs/xsl/docbook/html/lists.xsl | 1289 -- docs/xsl/docbook/html/maketoc.xsl | 87 - docs/xsl/docbook/html/manifest.xsl | 23 - docs/xsl/docbook/html/math.xsl | 272 - docs/xsl/docbook/html/oldchunker.xsl | 203 - docs/xsl/docbook/html/onechunk.xsl | 38 - docs/xsl/docbook/html/param.xml | 11308 ---------------- docs/xsl/docbook/html/param.xsl | 445 - docs/xsl/docbook/html/pi.xml | 1152 -- docs/xsl/docbook/html/pi.xsl | 1297 -- docs/xsl/docbook/html/profile-chunk-code.xsl | 638 - docs/xsl/docbook/html/profile-chunk.xsl | 53 - docs/xsl/docbook/html/profile-docbook.xsl | 507 - docs/xsl/docbook/html/profile-onechunk.xsl | 38 - docs/xsl/docbook/html/qandaset.xsl | 457 - docs/xsl/docbook/html/refentry.xsl | 307 - docs/xsl/docbook/html/sections.xsl | 638 - docs/xsl/docbook/html/synop.xsl | 1655 --- docs/xsl/docbook/html/table.xsl | 1210 -- docs/xsl/docbook/html/task.xsl | 79 - docs/xsl/docbook/html/titlepage.templates.xml | 738 - docs/xsl/docbook/html/titlepage.templates.xsl | 4005 ------ docs/xsl/docbook/html/titlepage.xsl | 1125 -- docs/xsl/docbook/html/toc.xsl | 354 - docs/xsl/docbook/html/verbatim.xsl | 411 - docs/xsl/docbook/html/xref.xsl | 1313 -- docs/xsl/docbook/lib/lib.xsl | 533 - docs/xsl/highlight/bourne-hl.xml | 95 - docs/xsl/highlight/gremlin-hl.xml | 39 - docs/xsl/highlight/highlight.xsl | 54 - docs/xsl/highlight/java-hl.xml | 117 - docs/xsl/highlight/javascript-hl.xml | 147 - docs/xsl/highlight/m2-hl.xml | 90 - docs/xsl/highlight/properties-hl.xml | 22 - docs/xsl/highlight/xslthl-config.xml | 12 - docs/xsl/single.xsl | 95 - .../util/system/ConfigurationPrinter.java | 39 +- janusgraph-dist/README.md | 11 +- janusgraph-dist/pom.xml | 163 +- .../descriptor/htmldocs.component.xml | 9 - .../assembly/descriptor/readmes.component.xml | 8 - .../src/release/gh-pages-update.sh | 106 - janusgraph-dist/src/release/index.html | 120 - janusgraph-dist/src/release/release.sh | 4 - janusgraph-doc/pom.xml | 210 +- mkdocs.yml | 109 + pom.xml | 195 - requirements.txt | 6 + 378 files changed, 9632 insertions(+), 142334 deletions(-) create mode 100644 .remarkrc.yml create mode 100644 docs.Dockerfile delete mode 100644 docs/adoc_attributes.adoc create mode 100644 docs/advanced-topics/advschema.md rename docs/{static/images => advanced-topics}/bigtablemodel.png (100%) create mode 100644 docs/advanced-topics/bulk-loading.md create mode 100644 docs/advanced-topics/data-model.md create mode 100644 docs/advanced-topics/eventual-consistency.md rename docs/{hadoop.adoc => advanced-topics/hadoop.md} (62%) create mode 100644 docs/advanced-topics/index-admin.md rename docs/{JanusGraphBus.md => advanced-topics/janusgraph-bus.md} (95%) create mode 100644 docs/advanced-topics/migrating.md create mode 100644 docs/advanced-topics/monitoring.md create mode 100644 docs/advanced-topics/partitioning.md create mode 100644 docs/advanced-topics/recovery.md rename docs/{static/images => advanced-topics}/relationlayout.png (100%) create mode 100644 docs/advanced-topics/serializer.md rename docs/{static/images => advanced-topics}/storagelayout.png (100%) delete mode 100644 docs/advanced.adoc delete mode 100644 docs/advblueprints.adoc delete mode 100644 docs/advschema.adoc delete mode 100644 docs/appendices.adoc create mode 100644 docs/appendices.md rename docs/{static/images => }/architecture-layer-diagram.svg (100%) delete mode 100644 docs/basics.adoc rename docs/{static/images => basics}/advanced-scenario.svg (100%) rename docs/{static/images => basics}/advanced-scenario.xml (100%) create mode 100644 docs/basics/cache.md create mode 100644 docs/basics/common-questions.md create mode 100644 docs/basics/configuration-reference.md create mode 100644 docs/basics/configuration.md rename docs/{configuredgraphfactory.adoc => basics/configured-graph-factory.md} (55%) create mode 100644 docs/basics/deployment.md create mode 100644 docs/basics/example-config.md rename docs/{static/images => basics}/getting-started-scenario.svg (100%) rename docs/{static/images => basics}/getting-started-scenario.xml (100%) create mode 100644 docs/basics/gremlin.md create mode 100644 docs/basics/index-performance.md create mode 100644 docs/basics/janusgraph-cfg.md rename docs/{static/images => basics}/minimalist-scenario.svg (100%) rename docs/{static/images => basics}/minimalist-scenario.xml (100%) create mode 100644 docs/basics/multi-node.md create mode 100644 docs/basics/schema.md create mode 100644 docs/basics/server.md create mode 100644 docs/basics/technical-limitations.md create mode 100644 docs/basics/transaction-log.md create mode 100644 docs/basics/transactions.md delete mode 100644 docs/bdb.adoc delete mode 100644 docs/bigtable.adoc delete mode 100755 docs/build-and-copy-docs.sh delete mode 100644 docs/building.adoc delete mode 100644 docs/bulkloading.adoc rename docs/{static/images => }/cassandra-small.svg (100%) delete mode 100644 docs/cassandra.adoc delete mode 100644 docs/changelog.adoc create mode 100644 docs/changelog.md delete mode 100644 docs/configref.adoc delete mode 100644 docs/connecting-via-dotnet.adoc delete mode 100644 docs/connecting-via-java.adoc delete mode 100644 docs/connecting-via-python.adoc delete mode 100644 docs/connecting.adoc create mode 100644 docs/connecting/dotnet.md create mode 100644 docs/connecting/index.md create mode 100644 docs/connecting/java.md create mode 100644 docs/connecting/python.md delete mode 100644 docs/datamodel.adoc delete mode 100644 docs/deploymentscenarios.adoc delete mode 100644 docs/development.adoc create mode 100644 docs/development.md delete mode 100644 docs/directindex.adoc delete mode 100644 docs/doc-versions.adoc delete mode 100644 docs/elasticsearch.adoc delete mode 100644 docs/eventualconsistency.adoc delete mode 100644 docs/exampleconfig.adoc create mode 100644 docs/favicon.ico delete mode 100644 docs/generating.adoc rename docs/{static/images => }/graph-of-the-gods-2.png (100%) delete mode 100644 docs/hbase.adoc create mode 100644 docs/images/logos/celum.png create mode 100644 docs/images/logos/compose.png create mode 100644 docs/images/logos/finc.png create mode 100644 docs/images/logos/gdata.png create mode 100644 docs/images/logos/qihoo_360.png create mode 100644 docs/images/logos/redhat.png create mode 100644 docs/images/logos/timesinternet.png create mode 100644 docs/index-backend/direct-index-query.md create mode 100644 docs/index-backend/elasticsearch.md create mode 100644 docs/index-backend/field-mapping.md create mode 100644 docs/index-backend/index.md create mode 100644 docs/index-backend/lucene.md rename docs/{searchpredicates.adoc => index-backend/search-predicates.md} (77%) create mode 100644 docs/index-backend/solr.md create mode 100644 docs/index-backend/text-search.md delete mode 100644 docs/index.adoc create mode 100644 docs/index.md delete mode 100644 docs/indexbackends.adoc delete mode 100644 docs/inmemorybackend.adoc delete mode 100644 docs/internals.adoc delete mode 100644 docs/intro.adoc create mode 100644 docs/janusgraph-logomark.svg create mode 100644 docs/janusgraph.png delete mode 100644 docs/listings/janusgraph_cfg.adoc delete mode 100644 docs/lucene.adoc delete mode 100644 docs/migrating.adoc delete mode 100644 docs/monitoring.adoc delete mode 100644 docs/partitioning.adoc delete mode 100644 docs/recovery.adoc delete mode 100644 docs/reindex.adoc delete mode 100644 docs/serializer.adoc delete mode 100644 docs/solr.adoc delete mode 100644 docs/static/css/docs.css delete mode 100644 docs/static/favicon.ico delete mode 100644 docs/static/images/icons/README delete mode 100644 docs/static/images/icons/callouts/1.png delete mode 100644 docs/static/images/icons/callouts/10.png delete mode 100644 docs/static/images/icons/callouts/11.png delete mode 100644 docs/static/images/icons/callouts/12.png delete mode 100644 docs/static/images/icons/callouts/13.png delete mode 100644 docs/static/images/icons/callouts/14.png delete mode 100644 docs/static/images/icons/callouts/15.png delete mode 100644 docs/static/images/icons/callouts/2.png delete mode 100644 docs/static/images/icons/callouts/3.png delete mode 100644 docs/static/images/icons/callouts/4.png delete mode 100644 docs/static/images/icons/callouts/5.png delete mode 100644 docs/static/images/icons/callouts/6.png delete mode 100644 docs/static/images/icons/callouts/7.png delete mode 100644 docs/static/images/icons/callouts/8.png delete mode 100644 docs/static/images/icons/callouts/9.png delete mode 100644 docs/static/images/icons/caution.png delete mode 100644 docs/static/images/icons/example.png delete mode 100644 docs/static/images/icons/home.png delete mode 100644 docs/static/images/icons/important.png delete mode 100644 docs/static/images/icons/next.png delete mode 100644 docs/static/images/icons/note.png delete mode 100644 docs/static/images/icons/prev.png delete mode 100644 docs/static/images/icons/tip.png delete mode 100644 docs/static/images/icons/up.png delete mode 100644 docs/static/images/icons/warning.png delete mode 100644 docs/static/images/janusgraph-logo.png delete mode 100644 docs/static/images/janusgraph-logomark.png delete mode 100644 docs/static/images/splash-graph.png delete mode 100644 docs/static/js/jquery/jquery-1.11.0.js delete mode 100644 docs/static/js/jquery/jquery-migrate-1.2.1.min.js create mode 100644 docs/storage-backend/bdb.md create mode 100644 docs/storage-backend/bigtable.md create mode 100644 docs/storage-backend/cassandra.md rename docs/{static/images/Cloud-Bigtable.svg => storage-backend/cloud-bigtable.svg} (100%) create mode 100644 docs/storage-backend/hbase.md create mode 100644 docs/storage-backend/index.md create mode 100644 docs/storage-backend/inmemorybackend.md rename docs/{static/images => storage-backend}/modes-distributed.png (100%) rename docs/{static/images => storage-backend}/modes-embedded.png (100%) rename docs/{static/images => storage-backend}/modes-local.png (100%) rename docs/{static/images => storage-backend}/modes-rexster.png (100%) delete mode 100644 docs/storagebackends.adoc delete mode 100644 docs/textsearch.adoc create mode 100644 docs/theme/extra.css create mode 100644 docs/theme/structor-menu.css create mode 100644 docs/theme/structor-menu.js.gotmpl delete mode 100644 docs/tutorials.adoc delete mode 100644 docs/upgrade.adoc delete mode 100644 docs/usefultools.adoc delete mode 100644 docs/versions.adoc delete mode 100644 docs/xsl/chunk.xsl delete mode 100644 docs/xsl/common.xsl delete mode 100644 docs/xsl/docbook/VERSION.xsl delete mode 100644 docs/xsl/docbook/common/addns.xsl delete mode 100644 docs/xsl/docbook/common/af.xml delete mode 100644 docs/xsl/docbook/common/am.xml delete mode 100644 docs/xsl/docbook/common/ar.xml delete mode 100644 docs/xsl/docbook/common/as.xml delete mode 100644 docs/xsl/docbook/common/ast.xml delete mode 100644 docs/xsl/docbook/common/autoidx-kimber.xsl delete mode 100644 docs/xsl/docbook/common/autoidx-kosek.xsl delete mode 100644 docs/xsl/docbook/common/az.xml delete mode 100644 docs/xsl/docbook/common/bg.xml delete mode 100644 docs/xsl/docbook/common/bn.xml delete mode 100644 docs/xsl/docbook/common/bn_in.xml delete mode 100644 docs/xsl/docbook/common/bs.xml delete mode 100644 docs/xsl/docbook/common/ca.xml delete mode 100644 docs/xsl/docbook/common/charmap.xml delete mode 100644 docs/xsl/docbook/common/charmap.xsl delete mode 100644 docs/xsl/docbook/common/common.xml delete mode 100644 docs/xsl/docbook/common/common.xsl delete mode 100644 docs/xsl/docbook/common/cs.xml delete mode 100644 docs/xsl/docbook/common/cy.xml delete mode 100644 docs/xsl/docbook/common/da.xml delete mode 100644 docs/xsl/docbook/common/de.xml delete mode 100644 docs/xsl/docbook/common/el.xml delete mode 100644 docs/xsl/docbook/common/en.xml delete mode 100644 docs/xsl/docbook/common/entities.ent delete mode 100644 docs/xsl/docbook/common/eo.xml delete mode 100644 docs/xsl/docbook/common/es.xml delete mode 100644 docs/xsl/docbook/common/et.xml delete mode 100644 docs/xsl/docbook/common/eu.xml delete mode 100644 docs/xsl/docbook/common/fa.xml delete mode 100644 docs/xsl/docbook/common/fi.xml delete mode 100644 docs/xsl/docbook/common/fr.xml delete mode 100644 docs/xsl/docbook/common/ga.xml delete mode 100644 docs/xsl/docbook/common/gentext.xsl delete mode 100644 docs/xsl/docbook/common/gl.xml delete mode 100644 docs/xsl/docbook/common/gu.xml delete mode 100644 docs/xsl/docbook/common/he.xml delete mode 100644 docs/xsl/docbook/common/hi.xml delete mode 100644 docs/xsl/docbook/common/hr.xml delete mode 100644 docs/xsl/docbook/common/hu.xml delete mode 100644 docs/xsl/docbook/common/id.xml delete mode 100644 docs/xsl/docbook/common/insertfile.xsl delete mode 100644 docs/xsl/docbook/common/is.xml delete mode 100644 docs/xsl/docbook/common/it.xml delete mode 100644 docs/xsl/docbook/common/ja.xml delete mode 100644 docs/xsl/docbook/common/ka.xml delete mode 100644 docs/xsl/docbook/common/kn.xml delete mode 100644 docs/xsl/docbook/common/ko.xml delete mode 100644 docs/xsl/docbook/common/ky.xml delete mode 100644 docs/xsl/docbook/common/l10n.dtd delete mode 100644 docs/xsl/docbook/common/l10n.xml delete mode 100644 docs/xsl/docbook/common/l10n.xsl delete mode 100644 docs/xsl/docbook/common/la.xml delete mode 100644 docs/xsl/docbook/common/labels.xsl delete mode 100644 docs/xsl/docbook/common/lt.xml delete mode 100644 docs/xsl/docbook/common/lv.xml delete mode 100644 docs/xsl/docbook/common/ml.xml delete mode 100644 docs/xsl/docbook/common/mn.xml delete mode 100644 docs/xsl/docbook/common/mr.xml delete mode 100644 docs/xsl/docbook/common/nb.xml delete mode 100644 docs/xsl/docbook/common/nds.xml delete mode 100644 docs/xsl/docbook/common/nl.xml delete mode 100644 docs/xsl/docbook/common/nn.xml delete mode 100644 docs/xsl/docbook/common/olink.xsl delete mode 100644 docs/xsl/docbook/common/or.xml delete mode 100644 docs/xsl/docbook/common/pa.xml delete mode 100644 docs/xsl/docbook/common/pi.xml delete mode 100644 docs/xsl/docbook/common/pi.xsl delete mode 100644 docs/xsl/docbook/common/pl.xml delete mode 100644 docs/xsl/docbook/common/pt.xml delete mode 100644 docs/xsl/docbook/common/pt_br.xml delete mode 100644 docs/xsl/docbook/common/refentry.xml delete mode 100644 docs/xsl/docbook/common/refentry.xsl delete mode 100644 docs/xsl/docbook/common/ro.xml delete mode 100644 docs/xsl/docbook/common/ru.xml delete mode 100644 docs/xsl/docbook/common/sk.xml delete mode 100644 docs/xsl/docbook/common/sl.xml delete mode 100644 docs/xsl/docbook/common/sq.xml delete mode 100644 docs/xsl/docbook/common/sr.xml delete mode 100644 docs/xsl/docbook/common/sr_Latn.xml delete mode 100644 docs/xsl/docbook/common/subtitles.xsl delete mode 100644 docs/xsl/docbook/common/sv.xml delete mode 100644 docs/xsl/docbook/common/ta.xml delete mode 100644 docs/xsl/docbook/common/table.xsl delete mode 100644 docs/xsl/docbook/common/targetdatabase.dtd delete mode 100644 docs/xsl/docbook/common/targets.xsl delete mode 100644 docs/xsl/docbook/common/te.xml delete mode 100644 docs/xsl/docbook/common/th.xml delete mode 100644 docs/xsl/docbook/common/titles.xsl delete mode 100644 docs/xsl/docbook/common/tl.xml delete mode 100644 docs/xsl/docbook/common/tr.xml delete mode 100644 docs/xsl/docbook/common/uk.xml delete mode 100644 docs/xsl/docbook/common/utility.xml delete mode 100644 docs/xsl/docbook/common/utility.xsl delete mode 100644 docs/xsl/docbook/common/vi.xml delete mode 100644 docs/xsl/docbook/common/xh.xml delete mode 100644 docs/xsl/docbook/common/zh.xml delete mode 100644 docs/xsl/docbook/common/zh_cn.xml delete mode 100644 docs/xsl/docbook/common/zh_tw.xml delete mode 100644 docs/xsl/docbook/highlighting/bourne-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/c-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/cmake-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/common.xsl delete mode 100644 docs/xsl/docbook/highlighting/cpp-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/csharp-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/css21-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/delphi-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/ini-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/java-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/javascript-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/lua-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/m2-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/myxml-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/perl-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/php-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/python-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/ruby-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/sql1999-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/sql2003-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/sql92-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/tcl-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/upc-hl.xml delete mode 100644 docs/xsl/docbook/highlighting/xslthl-config.xml delete mode 100644 docs/xsl/docbook/html/admon.xsl delete mode 100644 docs/xsl/docbook/html/annotations.xsl delete mode 100644 docs/xsl/docbook/html/autoidx-kimber.xsl delete mode 100644 docs/xsl/docbook/html/autoidx-kosek.xsl delete mode 100644 docs/xsl/docbook/html/autoidx-ng.xsl delete mode 100644 docs/xsl/docbook/html/autoidx.xsl delete mode 100644 docs/xsl/docbook/html/autotoc.xsl delete mode 100644 docs/xsl/docbook/html/biblio-iso690.xsl delete mode 100644 docs/xsl/docbook/html/biblio.xsl delete mode 100644 docs/xsl/docbook/html/block.xsl delete mode 100644 docs/xsl/docbook/html/callout.xsl delete mode 100644 docs/xsl/docbook/html/changebars.xsl delete mode 100644 docs/xsl/docbook/html/chunk-changebars.xsl delete mode 100644 docs/xsl/docbook/html/chunk-code.xsl delete mode 100644 docs/xsl/docbook/html/chunk-common.xsl delete mode 100644 docs/xsl/docbook/html/chunk.xsl delete mode 100644 docs/xsl/docbook/html/chunker.xsl delete mode 100644 docs/xsl/docbook/html/chunkfast.xsl delete mode 100644 docs/xsl/docbook/html/chunktoc.xsl delete mode 100644 docs/xsl/docbook/html/component.xsl delete mode 100644 docs/xsl/docbook/html/division.xsl delete mode 100644 docs/xsl/docbook/html/docbook.css.xml delete mode 100644 docs/xsl/docbook/html/docbook.xsl delete mode 100644 docs/xsl/docbook/html/ebnf.xsl delete mode 100644 docs/xsl/docbook/html/footnote.xsl delete mode 100644 docs/xsl/docbook/html/formal.xsl delete mode 100644 docs/xsl/docbook/html/glossary.xsl delete mode 100644 docs/xsl/docbook/html/graphics.xsl delete mode 100644 docs/xsl/docbook/html/highlight.xsl delete mode 100644 docs/xsl/docbook/html/html-rtf.xsl delete mode 100644 docs/xsl/docbook/html/html.xsl delete mode 100644 docs/xsl/docbook/html/htmltbl.xsl delete mode 100644 docs/xsl/docbook/html/index.xsl delete mode 100644 docs/xsl/docbook/html/info.xsl delete mode 100644 docs/xsl/docbook/html/inline.xsl delete mode 100644 docs/xsl/docbook/html/keywords.xsl delete mode 100644 docs/xsl/docbook/html/lists.xsl delete mode 100644 docs/xsl/docbook/html/maketoc.xsl delete mode 100644 docs/xsl/docbook/html/manifest.xsl delete mode 100644 docs/xsl/docbook/html/math.xsl delete mode 100644 docs/xsl/docbook/html/oldchunker.xsl delete mode 100644 docs/xsl/docbook/html/onechunk.xsl delete mode 100644 docs/xsl/docbook/html/param.xml delete mode 100644 docs/xsl/docbook/html/param.xsl delete mode 100644 docs/xsl/docbook/html/pi.xml delete mode 100644 docs/xsl/docbook/html/pi.xsl delete mode 100644 docs/xsl/docbook/html/profile-chunk-code.xsl delete mode 100644 docs/xsl/docbook/html/profile-chunk.xsl delete mode 100644 docs/xsl/docbook/html/profile-docbook.xsl delete mode 100644 docs/xsl/docbook/html/profile-onechunk.xsl delete mode 100644 docs/xsl/docbook/html/qandaset.xsl delete mode 100644 docs/xsl/docbook/html/refentry.xsl delete mode 100644 docs/xsl/docbook/html/sections.xsl delete mode 100644 docs/xsl/docbook/html/synop.xsl delete mode 100644 docs/xsl/docbook/html/table.xsl delete mode 100644 docs/xsl/docbook/html/task.xsl delete mode 100644 docs/xsl/docbook/html/titlepage.templates.xml delete mode 100644 docs/xsl/docbook/html/titlepage.templates.xsl delete mode 100644 docs/xsl/docbook/html/titlepage.xsl delete mode 100644 docs/xsl/docbook/html/toc.xsl delete mode 100644 docs/xsl/docbook/html/verbatim.xsl delete mode 100644 docs/xsl/docbook/html/xref.xsl delete mode 100644 docs/xsl/docbook/lib/lib.xsl delete mode 100644 docs/xsl/highlight/bourne-hl.xml delete mode 100644 docs/xsl/highlight/gremlin-hl.xml delete mode 100644 docs/xsl/highlight/highlight.xsl delete mode 100644 docs/xsl/highlight/java-hl.xml delete mode 100644 docs/xsl/highlight/javascript-hl.xml delete mode 100644 docs/xsl/highlight/m2-hl.xml delete mode 100644 docs/xsl/highlight/properties-hl.xml delete mode 100644 docs/xsl/highlight/xslthl-config.xml delete mode 100644 docs/xsl/single.xsl delete mode 100755 janusgraph-dist/src/release/gh-pages-update.sh delete mode 100644 janusgraph-dist/src/release/index.html create mode 100644 mkdocs.yml create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index a081308728..dd1772f866 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ coverity_tool.tar.gz cov-analysis-linux64/ cov-int.tar.gz cov-int/ + +/site/ \ No newline at end of file diff --git a/.remarkrc.yml b/.remarkrc.yml new file mode 100644 index 0000000000..446acd502f --- /dev/null +++ b/.remarkrc.yml @@ -0,0 +1,2 @@ +plugins: + lint-list-item-indent: mixed \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index fc803c6016..f823e0015d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -# Copyright 2017 JanusGraph Authors +# Copyright 2019 JanusGraph Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,14 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -group: travis_latest language: java -sudo: true +sudo: required +dist: trusty services: - docker jdk: - openjdk8 +git: + depth: false + cache: directories: - ${HOME}/.m2 @@ -29,48 +32,161 @@ env: # This is the encrypted COVERITY_SCAN_TOKEN, created via the # `travis encrypt` command using the project repo's public key. - secure: "v5ixqTeb74y0vRuPcDbe3C28GDDYvqyEXA2dt+9UVU6GG7WpnmpkBf05gI1dIhp51lBhwx9WSlFBtzho+KdCBmNY/CzBRhVHe/lCQYK9Hb6uGPvuwBvC0WjJgJXsVrLFjppeRhcf+OAweVQ3uw2RPMDRvKIVMUcO1BTFjjJl6REJXNUdzGS57MtH2mmRyOEz250EwgqUELZvcOytG7fNrjMJKVK2nSsoxi0BqZIpItTWPWWeQ1wi1FplJ18A2qtD+MPfAGNSB+/a+r0Av+VCT2eGl06ZyZAzP3q/vG5IYjQ3AJsSPqcZUt4ms+2us1+kwuzXIILjzZmcfImu29+y/thndU5E5b2v+nZ4H69CUCc5OmKW2RwozLNmBIUhO0n+35va/J7FiPIqm3pwxCz5vWA3YTHDADxnIYe7+9uY/+dOK/AvP5fyu7u07vuF3liKNBdrX7ylP3kYc7FXGmYl8wCZv31iy1yTtndQ9qKef7bo8lM9Cdh39KyowrygH+Um7pr9gqf2S9jn99nQ3bib32fBWgBkLpJRwhZYHPUupZjZfgu/9woby0DuriuHZKMqZd7QUawYz6wXGlhzu78x5Tohlj1pGBwHYdcJ/Tm3PiEpyH4aYQLffkjGHJAcCW5tO8QbB0qrLYWC8xVMWuFz1TpSBRXOqVYdBfIa2UZDtOU=" - - COVERITY_BRANCH_NAME="coverity_scan" - COVERITY_EMAIL="sjudeng@gmail.com" # Default Elasticsearch heap size can be too large for Travis - ES_JAVA_OPTS="-Xms256m -Xmx512m" - matrix: - # sort modules by test time with quickest modules first - - MODULE='hadoop-parent/janusgraph-hadoop-2' - - MODULE='lucene' - - MODULE='solr' ARGS='-Pdocker,solr7' - - MODULE='solr' ARGS='-Pdocker,solr6' - - MODULE='solr' ARGS='-Pdocker,solr5' - - MODULE='es' ARGS='-Pelasticsearch6' - - MODULE='es' ARGS='-Pelasticsearch5' - - MODULE='es' ARGS='-Pelasticsearch2' - - MODULE='es' ARGS='-Pelasticsearch1,es-docker' - - MODULE='berkeleyje' - - MODULE='test' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/thrift/* -Dtest.skip.ordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ordered=true' - - MODULE='cassandra' ARGS='-Dtest=**/graphdb/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' - - MODULE='cassandra' ARGS='-Dtest=**/graphdb/thrift/* -Dtest.skip.ordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' - - MODULE='cassandra' ARGS='-Dtest=**/graphdb/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ordered=true' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/astyanax/* -Dtest.skip.unordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/astyanax/* -Dtest.skip.ordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/astyanax/* -Dtest.skip.unordered=true -Dtest.skip.ordered=true' - - MODULE='cassandra' ARGS='-Dtest=**/graphdb/astyanax/*' - - MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/embedded/*' - - MODULE='cassandra' ARGS='-Dtest=***/cassandra/*,*/graphdb/embedded/*' - - MODULE='hbase-parent/janusgraph-hbase-10' ARGS='-Dtest=**/diskstorage/hbase/*' - - MODULE='hbase-parent/janusgraph-hbase-10' ARGS='-Dtest=**/graphdb/hbase/*' - - MODULE='hbase-parent/janusgraph-hbase-098' ARGS='-Dtest=**/diskstorage/hbase/*' - - MODULE='hbase-parent/janusgraph-hbase-098' ARGS='-Dtest=**/graphdb/hbase/*' - - MODULE='cql' ARGS='-Dtest=**/diskstorage/cql/* -Dtest.skip.murmur=true' - - MODULE='cql' ARGS='-Dtest=**/diskstorage/cql/* -Dtest.skip.byteorderedpartitioner=true -Dtest.skip.murmur-serial=true -Dtest.skip.murmur-ssl=true' - - MODULE='cql' ARGS='-Dtest=**/graphdb/cql/* -Dtest.skip.murmur=true' - - MODULE='cql' ARGS='-Dtest=**/graphdb/cql/* -Dtest.skip.byteorderedpartitioner=true -Dtest.skip.murmur-serial=true -Dtest.skip.murmur-ssl=true' - - COVERITY_ONLY=true +stages: + - test + - documentation + - deploy + +jobs: + include: + - &STANDARD_TEST_JOB + stage: test + env: MODULE='hadoop-parent/janusgraph-hadoop-2' + install: travis_wait mvn clean install --projects janusgraph-${MODULE} --also-make -DskipTests=true -Dmaven.javadoc.skip=true --batch-mode --show-version; + script: travis_retry travis_wait 50 mvn clean verify --projects janusgraph-${MODULE} -Pcoverage ${ARGS}; + after_success: bash <(curl -s https://codecov.io/bash); + if: commit_message !~ /\[doc only\]/ + - + <<: *STANDARD_TEST_JOB + env: MODULE='lucene' + - + <<: *STANDARD_TEST_JOB + env: MODULE='solr' ARGS='-Pdocker,solr7' + - + <<: *STANDARD_TEST_JOB + env: MODULE='solr' ARGS='-Pdocker,solr6' + - + <<: *STANDARD_TEST_JOB + env: MODULE='solr' ARGS='-Pdocker,solr5' + - + <<: *STANDARD_TEST_JOB + env: MODULE='es' ARGS='-Pelasticsearch6' + - + <<: *STANDARD_TEST_JOB + env: MODULE='es' ARGS='-Pelasticsearch5' + - + <<: *STANDARD_TEST_JOB + env: MODULE='es' ARGS='-Pelasticsearch2' + - + <<: *STANDARD_TEST_JOB + env: MODULE='es' ARGS='-Pelasticsearch1,es-docker' + - + <<: *STANDARD_TEST_JOB + env: MODULE='berkeleyje' + - + <<: *STANDARD_TEST_JOB + env: MODULE='test' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/thrift/* -Dtest.skip.ordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ordered=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/graphdb/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/graphdb/thrift/* -Dtest.skip.ordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/graphdb/thrift/* -Dtest.skip.unordered=true -Dtest.skip.ordered=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/astyanax/* -Dtest.skip.unordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/astyanax/* -Dtest.skip.ordered=true -Dtest.skip.ssl=true -Dtest.skip.serial=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/astyanax/* -Dtest.skip.unordered=true -Dtest.skip.ordered=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/graphdb/astyanax/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=**/diskstorage/cassandra/embedded/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cassandra' ARGS='-Dtest=***/cassandra/*,*/graphdb/embedded/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='hbase-parent/janusgraph-hbase-10' ARGS='-Dtest=**/diskstorage/hbase/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='hbase-parent/janusgraph-hbase-10' ARGS='-Dtest=**/graphdb/hbase/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='hbase-parent/janusgraph-hbase-098' ARGS='-Dtest=**/diskstorage/hbase/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='hbase-parent/janusgraph-hbase-098' ARGS='-Dtest=**/graphdb/hbase/*' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cql' ARGS='-Dtest=**/diskstorage/cql/* -Dtest.skip.murmur=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cql' ARGS='-Dtest=**/diskstorage/cql/* -Dtest.skip.byteorderedpartitioner=true -Dtest.skip.murmur-serial=true -Dtest.skip.murmur-ssl=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cql' ARGS='-Dtest=**/graphdb/cql/* -Dtest.skip.murmur=true' + - + <<: *STANDARD_TEST_JOB + env: MODULE='cql' ARGS='-Dtest=**/graphdb/cql/* -Dtest.skip.byteorderedpartitioner=true -Dtest.skip.murmur-serial=true -Dtest.skip.murmur-ssl=true' + + - stage: test + env: COVERITY_ONLY=true + install: travis_wait mvn --quiet install -DskipTests=true -Dmaven.javadoc.skip=true --batch-mode --show-version; + script: + - echo "Building Docker image for Coverity analysis"; + docker build -t janusgraph/analysis analysis; + echo "Running Coverity scan"; + travis_wait 50 \ + docker run --rm \ + -v ${HOME}/.m2:/root/.m2 -v ${PWD}:/opt/janusgraph \ + -e COVERITY_SCAN_TOKEN="${COVERITY_SCAN_TOKEN}" \ + -e COVERITY_EMAIL="${COVERITY_EMAIL}" \ + -i janusgraph/analysis; + if: branch = coverity_scan + + - stage: documentation + install: docker build -t doc-site:mkdocs -f docs.Dockerfile . + script: + - echo "Updating configuration markdown"; + mvn --quiet clean install -DskipTests=true -pl janusgraph-doc -am; + - docker run --rm -v $PWD:/mkdocs doc-site:mkdocs mkdocs build + + - stage: deploy + install: skip + script: skip + before_deploy: + - echo "Updating configuration markdown"; + mvn --quiet clean install -DskipTests=true -pl janusgraph-doc -am; + - echo "Download documentation generator"; + curl -sfL https://raw.githubusercontent.com/containous/structor/master/godownloader.sh | bash -s -- -b $GOPATH/bin v1.7.1 + - echo "Build documentation"; + structor -o janusgraph -r janusgraph \ + --dockerfile-url="https://raw.githubusercontent.com/janusgraph/janusgraph/v0.2/docs.Dockerfile" \ + --menu.js-url="https://raw.githubusercontent.com/janusgraph/janusgraph/v0.2/docs/theme/structor-menu.js.gotmpl" \ + --exp-branch=v0.2 --debug; + deploy: + provider: pages + repo: janusgraph/janusgraph + edge: false + github_token: ${GITHUB_TOKEN} + local_dir: site + skip_cleanup: true + on: + all_branches: true + condition: $TRAVIS_BRANCH =~ ^master$|^v[0-9.]+$ -matrix: - fast_finish: true # https://docs.travis-ci.com/user/customizing-the-build#Rows-that-are-Allowed-to-Fail allow_failures: # Elasticsearch 1.x tests can fail non-deterministically @@ -89,45 +205,7 @@ matrix: - env: MODULE='cql' ARGS='-Dtest=**/graphdb/cql/* -Dtest.skip.murmur=true' - env: MODULE='cql' ARGS='-Dtest=**/graphdb/cql/* -Dtest.skip.byteorderedpartitioner=true -Dtest.skip.murmur-serial=true -Dtest.skip.murmur-ssl=true' -install: - - if [ "${TRAVIS_BRANCH}" == "${COVERITY_BRANCH_NAME}" ] && ! [ -z "${COVERITY_ONLY:-}" ]; then - echo "Building all modules for Coverity analysis"; - travis_retry travis_wait mvn install -DskipTests=true -Dmaven.javadoc.skip=true --batch-mode --show-version; - elif [ "${TRAVIS_BRANCH}" == "${COVERITY_BRANCH_NAME}" ] || ! [ -z "${COVERITY_ONLY:-}" ]; then - echo "Building all modules for test-compile coverage, but skipping Coverity upload"; - travis_retry travis_wait mvn install -DskipTests=true -Dmaven.javadoc.skip=true --batch-mode --show-version; - else - echo "Building janusgraph-${MODULE} and dependencies"; - travis_retry travis_wait mvn install --projects janusgraph-${MODULE} --also-make -DskipTests=true -Dmaven.javadoc.skip=true --batch-mode --show-version; - fi - -script: - - if [ "${TRAVIS_BRANCH}" == "${COVERITY_BRANCH_NAME}" ] && ! [ -z "${COVERITY_ONLY:-}" ]; then - echo "Building Docker image for Coverity analysis"; - docker build -t janusgraph/analysis analysis; - echo "Running Coverity scan"; - travis_wait 50 \ - docker run --rm \ - -v ${HOME}/.m2:/root/.m2 -v ${PWD}:/opt/janusgraph \ - -e COVERITY_SCAN_TOKEN="${COVERITY_SCAN_TOKEN}" \ - -e COVERITY_EMAIL="${COVERITY_EMAIL}" \ - -i janusgraph/analysis; - elif [ "${TRAVIS_BRANCH}" == "${COVERITY_BRANCH_NAME}" ] || ! [ -z "${COVERITY_ONLY:-}" ]; then - echo "Skipping module tests on Coverity branch/job"; - else - echo "Testing janusgraph-${MODULE}"; - travis_retry travis_wait 50 mvn clean verify --projects janusgraph-${MODULE} -Pcoverage ${ARGS}; - fi - -after_success: - # Upload test coverage reports to Codecov - - if [ "${COVERITY_SCAN_BRANCH}" == 1 ] || [ -v COVERITY_ONLY ]; then - echo "Skipping test coverage report upload in Coverity branch/job"; - else - bash <(curl -s https://codecov.io/bash); - fi - # Syntax and more info: https://docs.travis-ci.com/user/notifications notifications: email: - - janusgraph-ci@googlegroups.com + - janusgraph-ci@googlegroups.com \ No newline at end of file diff --git a/BUILDING.md b/BUILDING.md index ce2f18085c..ece09216e4 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,5 +1,4 @@ -Building JanusGraph --------------- +# Building JanusGraph Required: @@ -101,3 +100,16 @@ To find the Java binary in your environment, run the appropriate command for you * Linux/macOS: `which java` * Windows: `for %i in (java.exe) do @echo. %~$PATH:i` +## Building documentation + +### Required dependencies to build the documentation +MkDocs need to be installed to build and serve the documentation locally. + +1. Install `python3` and `pip3` (newest version of pip) + * You can also checkout the installation guide of [material-mkdocs](https://squidfunk.github.io/mkdocs-material/getting-started/) +2. Install requirements using `pip3 install -r requirements.txt` + +### Build and serve documentation + +1. To create a test build locally use command `mkdocs build` +2. To serve the documentation locally use command `mkdocs serve` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d5058cb88..48b60c96be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,7 +131,7 @@ you've signed the contribution. > `git ci -v` will get you a diff of your commit while writing your commit > message. -> Note: If this is a non-code change, e.g. documentation, add `[skip ci]` to the +> Note: If this is a non-code change, e.g. documentation, add `[doc only]` to the > PR subject line. This is to save CPU time on Travis CI, which lets us get more > build time for the other changes which actually change the code. > @@ -140,7 +140,7 @@ you've signed the contribution. > * when the PR is submitted for review > * when the PR is merged to the base branch > -> Having [skip ci] in the commit skips the first one, but the merge commit also +> Having [doc only] in the commit skips the first one, but the merge commit also > needs it, so having it in the title (first line of commit) helps it easily > propagate to both places. diff --git a/RELEASING.md b/RELEASING.md index a372d1e393..c49f1a5487 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,8 +1,6 @@ -Releasing JanusGraph -==================== +# Releasing JanusGraph -Prerequisites -------------- +## Prerequisites The release process has only been tested on Linux. The following tools must be installed. @@ -29,26 +27,17 @@ tools must be installed. ``` -Release Steps -------------- +## Release Steps -### Preparing Documentation +### Release Documentation update -Update version-sensitive files in the root and documentation sources -in the `docs` subdirectory: - -* `docs/changelog.txt` -* `docs/versions.txt` -* `docs/upgrade.txt` - -Use the [`docs/build-and-copy-docs.sh`](docs/build-and-copy-docs.sh) script to -build a set of docs for this release and copy them to the cloned -`docs.janusgraph.org` repo which you will update later. - -You may also need to update the following files in the main repo for any new -or updated dependencies: - -* `NOTICE.txt` +1. Update version-sensitive files in the root and documentation sources in the `docs` subdirectory: + * `docs/changelog.md` + * `mkdocs.yml` +2. Update the configuration reference: `mvn install -DskipTests=true -pl janusgraph-doc -am` +3. For building documentation: see `building.md` +4. Zip documentation: `$ zip janusgraph-${JANUSGRAPH_VERSION}-hadoop2-doc.zip site` +5. You may also need to update the following file in the main repo for any new or updated dependencies: `NOTICE.txt` ### Preparing the Local Repository @@ -68,8 +57,6 @@ Recommended but not required: # * Locally commits the release using the release plugin # * Deploys Maven artifacts to Sonatype OSS (staging, not released yet) # * Uploads zipfiles to S3 -# * Locally commits gh-pages updates (index.html and javadocs) -# * Uploads AsciiDoc-generated documentation to S3 # # Although it uploads to Sonatype OSS Staging and S3, it does # not push to github nor does it release the Sonatype repo. @@ -103,7 +90,6 @@ Finally, push your local changes to Github: # cd to the janusgraph repository root if not already there git push origin $BRANCH_NAME git push origin refs/tags/$RELEASE_VERSION -git push origin gh-pages ``` Update these pages on the Github wiki: diff --git a/docs.Dockerfile b/docs.Dockerfile new file mode 100644 index 0000000000..a7419098ba --- /dev/null +++ b/docs.Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:3.9 + +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin + +COPY requirements.txt /mkdocs/ +WORKDIR /mkdocs +VOLUME /mkdocs + +RUN apk add --no-cache python3 git && \ + python3 -m ensurepip && \ + rm -r /usr/lib/python*/ensurepip && \ + pip3 install --upgrade pip==19.0.3 setuptools==40.8.0 && \ + if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi && \ + if [ ! -e /usr/bin/python ]; then ln -sf /usr/bin/python3 /usr/bin/python; fi && \ + rm -r /root/.cache + +RUN pip install --user -r requirements.txt \ No newline at end of file diff --git a/docs/adoc_attributes.adoc b/docs/adoc_attributes.adoc deleted file mode 100644 index 7e1a6b54fa..0000000000 --- a/docs/adoc_attributes.adoc +++ /dev/null @@ -1,3 +0,0 @@ -// This file is reserved for Asciidoc attribute definitions -// http://www.methods.co.nz/asciidoc/chunked/ch28.html -:tinkerpop_version: $MAVEN{tinkerpop.version} diff --git a/docs/advanced-topics/advschema.md b/docs/advanced-topics/advschema.md new file mode 100644 index 0000000000..bb481cc1d1 --- /dev/null +++ b/docs/advanced-topics/advschema.md @@ -0,0 +1,154 @@ +Advanced Schema +=============== + +This page describes some of the advanced schema definition options that +JanusGraph provides. For general information on JanusGraph’s schema and +how to define it, refer to [Schema and Data Modeling](../basics/schema.md). + +Static Vertices +--------------- + +Vertex labels can be defined as **static** which means that vertices +with that label cannot be modified outside the transaction in which they +were created. +```groovy +mgmt = graph.openManagement() +tweet = mgmt.makeVertexLabel('tweet').setStatic().make() +mgmt.commit() +``` + +Static vertex labels are a method of controlling the data lifecycle and +useful when loading data into the graph that should not be modified +after its creation. + +Edge and Vertex TTL +------------------- + +Edge and vertex labels can be configured with a **time-to-live (TTL)**. +Edges and vertices with such labels will automatically be removed from +the graph when the configured TTL has passed after their initial +creation. TTL configuration is useful when loading a large amount of +data into the graph that is only of temporary use. Defining a TTL +removes the need for manual clean up and handles the removal very +efficiently. For example, it would make sense to TTL event edges such as +user-page visits when those are summarized after a certain period of +time or simply no longer needed for analytics or operational query +processing. + +The following storage backends support edge and vertex TTL. + +- Cassandra + +- HBase + +### Edge TTL + +Edge TTL is defined on a per-edge label basis, meaning that all edges of +that label have the same time-to-live. Note that the backend must +support cell level TTL. Currently only Cassandra and HBase support this. +```groovy +mgmt = graph.openManagement() +visits = mgmt.makeEdgeLabel('visits').make() +mgmt.setTTL(visits, Duration.ofDays(7)) +mgmt.commit() +``` + +Note, that modifying an edge resets the TTL for that edge. Also note, +that the TTL of an edge label can be modified but it might take some +time for this change to propagate to all running JanusGraph instances +which means that two different TTLs can be temporarily in use for the +same label. + +### Property TTL + +Property TTL is very similar to edge TTL and defined on a per-property +key basis, meaning that all properties of that key have the same +time-to-live. Note that the backend must support cell level TTL. +Currently only Cassandra and HBase support this. +```groovy +mgmt = graph.openManagement() +sensor = mgmt.makePropertyKey('sensor').cardinality(Cardinality.LIST).dataType(Double.class).make() +mgmt.setTTL(sensor, Duration.ofDays(21)) +mgmt.commit() +``` + +As with edge TTL, modifying an existing property resets the TTL for that +property and modifying the TTL for a property key might not immediately +take effect. + +### Vertex TTL + +Vertex TTL is defined on a per-vertex label basis, meaning that all +vertices of that label have the same time-to-live. The configured TTL +applies to the vertex, its properties, and all incident edges to ensure +that the entire vertex is removed from the graph. For this reason, a +vertex label must be defined as *static* before a TTL can be set to rule +out any modifications that would invalidate the vertex TTL. Vertex TTL +only applies to static vertex labels. Note that the backend must support +store level TTL. Currently only Cassandra and HBase support this. +```groovy +mgmt = graph.openManagement() +tweet = mgmt.makeVertexLabel('tweet').setStatic().make() +mgmt.setTTL(tweet, Duration.ofHours(36)) +mgmt.commit() +``` + +Note, that the TTL of a vertex label can be modified but it might take +some time for this change to propagate to all running JanusGraph +instances which means that two different TTLs can be temporarily in use +for the same label. + +Multi-Properties +---------------- + +As discussed in [Schema and Data Modeling](../basics/schema.md), JanusGraph supports property keys with +SET and LIST cardinality. Hence, JanusGraph supports multiple properties +with the same key on a single vertex. Furthermore, JanusGraph treats +properties similarly to edges in that single-valued property annotations +are allowed on properties as shown in the following example. +```groovy +mgmt = graph.openManagement() +mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.LIST).make() +mgmt.commit() +v = graph.addVertex() +p1 = v.property('name', 'Dan LaRocque') +p1.property('source', 'web') +p2 = v.property('name', 'dalaro') +p2.property('source', 'github') +graph.tx().commit() +v.properties('name') +==> Iterable over all name properties +``` +These features are useful in a number of applications such as those +where attaching provenance information (e.g. who added a property, when +and from where?) to properties is necessary. Support for higher +cardinality properties and property annotations on properties is also +useful in high-concurrency, scale-out design patterns as described in +[Eventually-Consistent Storage Backends](eventual-consistency.md). + +Vertex-centric indexes and global graph indexes are supported for +properties in the same manner as they are supported for edges. Refer to +[Indexing for Better Performance](../basics/index-performance.md) for information on defining these indexes for edges and +use the corresponding API methods to define the same indexes for +properties. + +Unidirected Edges +----------------- + +Unidirected edges are edges that can only be traversed in the out-going +direction. Unidirected edges have a lower storage footprint but are +limited in the types of traversals they support. Unidirected edges are +conceptually similar to hyperlinks in the world-wide-web in the sense +that the out-vertex can traverse through the edge, but the in-vertex is +unaware of its existence. +```groovy +mgmt = graph.openManagement() +mgmt.makeEdgeLabel('author').unidirected().make() +mgmt.commit() +``` + +Note, that unidirected edges do not get automatically deleted when their +in-vertices are deleted. The user must ensure that such inconsistencies +do not arise or resolve them at query time by explicitly checking vertex +existence in a transaction. See the discussion in [Ghost Vertices](../basics/common-questions.md#ghost-vertices) +for more information. diff --git a/docs/static/images/bigtablemodel.png b/docs/advanced-topics/bigtablemodel.png similarity index 100% rename from docs/static/images/bigtablemodel.png rename to docs/advanced-topics/bigtablemodel.png diff --git a/docs/advanced-topics/bulk-loading.md b/docs/advanced-topics/bulk-loading.md new file mode 100644 index 0000000000..aef0936f61 --- /dev/null +++ b/docs/advanced-topics/bulk-loading.md @@ -0,0 +1,205 @@ +Bulk Loading +============ + +There are a number of configuration options and tools that make +ingesting large amounts of graph data into JanusGraph more efficient. +Such ingestion is referred to as *bulk loading* in contrast to the +default *transactional loading* where small amounts of data are added +through individual transactions. + +There are a number of use cases for bulk loading data into JanusGraph, +including: + +- Introducing JanusGraph into an existing environment with existing + data and migrating or duplicating this data into a new JanusGraph + cluster. + +- Using JanusGraph as an end point of an + [ETL](http://en.wikipedia.org/wiki/Extract,_transform,_load) + process. + +- Adding an existing or external graph datasets (e.g. publicly + available [RDF datasets](http://linkeddata.org/)) to a running + JanusGraph cluster. + +- Updating a JanusGraph graph with results from a graph analytics job. + +This page describes configuration options and tools that make bulk +loading more efficient in JanusGraph. Please observe the limitations and +assumptions for each option carefully before proceeding to avoid data +loss or data corruption. + +This documentation focuses on JanusGraph specific optimization. In +addition, consider improving the chosen storage backend and (optional) +index backend for high write performance. Please refer to the +documentation of the respective backend for more information. + +Configuration Options +--------------------- + +### Batch Loading + +Enabling the `storage.batch-loading` configuration option will have the +biggest positive impact on bulk loading times for most applications. +Enabling batch loading disables JanusGraph internal consistency checks +in a number of places. Most importantly, it disables locking. In other +words, JanusGraph assumes that the data to be loaded into JanusGraph is +consistent with the graph and hence disables its own checks in the +interest of performance. + +In many bulk loading scenarios it is significantly cheaper to ensure +data consistency prior to loading the data then ensuring data +consistency while loading it into the database. The +`storage.batch-loading` configuration option exists because of this +observation. + +For example, consider the use case of bulk loading existing user +profiles into JanusGraph. Furthermore, assume that the username property +key has a unique composite index defined on it, i.e. usernames must be +unique across the entire graph. If the user profiles are imported from +another database, username uniqueness might already guaranteed. If not, +it is simple to sort the profiles by name and filter out duplicates or +writing a Hadoop job that does such filtering. Now, we can enable +`storage.batch-loading` which significantly reduces the bulk loading +time because JanusGraph does not have to check for every added user +whether the name already exists in the database. + +**Important**: Enabling `storage.batch-loading` requires the user to +ensure that the loaded data is internally consistent and consistent with +any data already in the graph. In particular, concurrent type creation +can lead to severe data integrity issues when batch loading is enabled. +Hence, we **strongly** encourage disabling automatic type creation by +setting `schema.default = none` in the graph configuration. + +### Optimizing ID Allocation + +#### ID Block Size + +Each newly added vertex or edge is assigned a unique id. JanusGraph’s id +pool manager acquires ids in blocks for a particular JanusGraph +instance. The id block acquisition process is expensive because it needs +to guarantee globally unique assignment of blocks. Increasing +`ids.block-size` reduces the number of acquisitions but potentially +leaves many ids unassigned and hence wasted. For transactional workloads +the default block size is reasonable, but during bulk loading vertices +and edges are added much more frequently and in rapid succession. Hence, +it is generally advisable to increase the block size by a factor of 10 +or more depending on the number of vertices to be added per machine. + +**Rule of thumb**: Set `ids.block-size` to the number of vertices you +expect to add per JanusGraph instance per hour. + +**Important:** All JanusGraph instances MUST be configured with the same +value for `ids.block-size` to ensure proper id allocation. Hence, be +careful to shut down all JanusGraph instances prior to changing this +value. + +#### ID Acquisition Process + +When id blocks are frequently allocated by many JanusGraph instances in +parallel, allocation conflicts between instances will inevitably arise +and slow down the allocation process. In addition, the increased write +load due to bulk loading may further slow down the process to the point +where JanusGraph considers it failed and throws an exception. There are +three configuration options that can be tuned to avoid this. + +1) `ids.authority.wait-time` configures the time in milliseconds the id +pool manager waits for an id block application to be acknowledged by the +storage backend. The shorter this time, the more likely it is that an +application will fail on a congested storage cluster. + +**Rule of thumb**: Set this to the sum of the 95th percentile read and +write times measured on the storage backend cluster under load. +**Important**: This value should be the same across all JanusGraph +instances. + +2) `ids.renew-timeout` configures the number of milliseconds +JanusGraph’s id pool manager will wait in total while attempting to +acquire a new id block before failing. + +**Rule of thumb**: Set this value to be as large feasible to not have to +wait too long for unrecoverable failures. The only downside of +increasing it is that JanusGraph will try for a long time on an +unavailable storage backend cluster. + +### Optimizing Writes and Reads + +#### Buffer Size + +JanusGraph buffers writes and executes them in small batches to reduce +the number of requests against the storage backend. The size of these +batches is controlled by `storage.buffer-size`. When executing a lot of +writes in a short period of time, it is possible that the storage +backend can become overloaded with write requests. In that case, +increasing `storage.buffer-size` can avoid failure by increasing the +number of writes per request and thereby lowering the number of +requests. + +However, increasing the buffer size increases the latency of the write +request and its likelihood of failure. Hence, it is not advisable to +increase this setting for transactional loads and one should carefully +experiment with this setting during bulk loading. + +#### Read and Write Robustness + +During bulk loading, the load on the cluster typically increases making +it more likely for read and write operations to fail (in particular if +the buffer size is increased as described above). +`storage.read-attempts` and `storage.write-attempts` configure how many +times JanusGraph will attempt to execute a read or write operation +against the storage backend before giving up. If it is expected that +there is a high load on the backend during bulk loading, it is generally +advisable to increase these configuration options. + +`storage.attempt-wait` specifies the number of milliseconds that +JanusGraph will wait before re-attempting a failed backend operation. A +higher value can ensure that operation re-tries do not further increase +the load on the backend. + +Strategies +---------- + +### Parallelizing the Load + +By parallelizing the bulk loading across multiple machines, the load +time can be greatly reduced if JanusGraph’s storage backend cluster is +large enough to serve the additional requests. This is essentially the +approach [JanusGraph with TinkerPop’s Hadoop-Gremlin](hadoop.md) takes to bulk loading data into JanusGraph +using MapReduce. + +If Hadoop cannot be used for parallelizing the bulk loading process, +here are some high level guidelines for effectively parallelizing the +loading process: + +- In some cases, the graph data can be decomposed into multiple + disconnected subgraphs. Those subgraphs can be loaded independently + in parallel across multiple machines (for instance, using BatchGraph + as described above). + +- If the graph cannot be decomposed, it is often beneficial to load in + multiple steps where the last two steps can be parallelized across + multiple machines: + + 1. Make sure the vertex and edge data sets are de-duplicated and + consistent. + + 2. Set `batch-loading=true`. Possibly optimize additional + configuration settings described above. + + 3. Add all the vertices with their properties to the graph (but no + edges). Maintain a (distributed) map from vertex id (as defined + by the loaded data) to JanusGraph’s internal vertex id (i.e. + `vertex.getId()`) which is a 64 bit long id. + + 4. Add all the edges using the map to look-up JanusGraph’s vertex + id and retrieving the vertices using that id. + +Q&A +--- + +- **What should I do to avoid the following exception during + batch-loading:** + `java.io.IOException: ID renewal thread on partition [X] did not complete in time.`? + This exception is mostly likely caused by repeated time-outs during + the id allocation phase due to highly stressed storage backend. + Refer to the section on *ID Allocation Optimization* above. diff --git a/docs/advanced-topics/data-model.md b/docs/advanced-topics/data-model.md new file mode 100644 index 0000000000..b9847a1310 --- /dev/null +++ b/docs/advanced-topics/data-model.md @@ -0,0 +1,104 @@ +# JanusGraph Data Model + +JanusGraph stores graphs in [adjacency list +format](http://en.wikipedia.org/wiki/Adjacency_list) which means that a +graph is stored as a collection of vertices with their adjacency list. +The adjacency list of a vertex contains all of the vertex’s incident +edges (and properties). + +By storing a graph in adjacency list format JanusGraph ensures that all +of a vertex’s incident edges and properties are stored compactly in the +storage backend which speeds up traversals. The downside is that each +edge has to be stored twice - once for each end vertex of the edge. + +In addition, JanusGraph maintains the adjacency list of each vertex in +sort order with the order being defined by the sort key and sort order +the edge labels. The sort order enables efficient retrievals of subsets +of the adjacency list using [vertex centric indices](#vertex-indexes). + +JanusGraph stores the adjacency list representation of a graph in any +[storage backend](#storage-backends) that supports the Bigtable data +model. + +## Bigtable Data Model + +![](bigtablemodel.png) + +Under the [Bigtable data model](http://en.wikipedia.org/wiki/Bigtable) +each table is a collection of rows. Each row is uniquely identified by a +key. Each row is comprised of an arbitrary (large, but limited) number +of cells. A cell is composed of a column and value. A cell is uniquely +identified by a column within a given row. Rows in the Bigtable model +are called "wide rows" because they support a large number of cells and +the columns of those cells don’t have to be defined up front as is +required in relational databases. + +JanusGraph has an additional requirement for the Bigtable data model: +The cells must be sorted by their columns and a subset of the cells +specified by a column range must be efficiently retrievable (e.g. by +using index structures, skip lists, or binary search). + +In addition, a particular Bigtable implementation may keep the rows +sorted in the order of their key. JanusGraph can exploit such key-order +to effectively partition the graph which provides better loading and +traversal performance for very large graphs. However, this is not a +requirement. + +## JanusGraph Data Layout + +![](storagelayout.png) + +JanusGraph stores each adjacency list as a row in the underlying storage +backend. The (64 bit) vertex id (which JanusGraph uniquely assigns to +every vertex) is the key which points to the row containing the vertex’s +adjacency list. Each edge and property is stored as an individual cell +in the row which allows for efficient insertions and deletions. The +maximum number of cells allowed per row in a particular storage backend +is therefore also the maximum degree of a vertex that JanusGraph can +support against this backend. + +If the storage backend supports key-order, the adjacency lists will be +ordered by vertex id, and JanusGraph can assign vertex ids such that the +graph is effectively partitioned. Ids are assigned such that vertices +which are frequently co-accessed have ids with small absolute +difference. + +## Individual Edge Layout + +![](relationlayout.png) + +Each edge and property is stored as one cell in the rows of its adjacent +vertices. They are serialized such that the byte order of the column +respects the sort key of the edge label. Variable id encoding schemes +and compressed object serialization are used to keep the storage +footprint of each edge/cell as small as possible. + +Consider the storage layout of an individual edge as visualized in the +top row of the graphic above. The dark blue boxes represent numbers that +are encoded with a variable length encoding scheme to reduce the number +of bytes they consume. Red boxes represent one or multiple property +values (i.e. objects) that are serialized with compressed meta data +referenced in the associated property key. Grey boxes represent +uncompressed property values (i.e. serialized objects). + +The serialized representation of an edge starts with the edge label’s +unique id (as assigned by JanusGraph). This is typically a small number +and compressed well with variable id encoding. The last bit of this id +is offset to store whether this is an incoming or outgoing edge. Next, +the property value comprising the sort key are stored. The sort key is +defined with the edge label and hence the sort key objects meta data can +be referenced to the edge label. After that, the id of the adjacent +vertex is stored. JanusGraph does not store the actual vertex id but the +difference to the id of the vertex that owns this adjacency list. It is +likely that the difference is a smaller number than the absolute id and +hence compresses better. The vertex id is followed by the id of this +edge. Each edge is assigned a unique id by JanusGraph. This concludes +the column value of the edge’s cell. The value of the edge’s cell +contains the compressed serialization of the signature properties of the +edge (as defined by the label’s signature key) and any other properties +that have been added to the edge in uncompressed serialization. + +The serialized representation of a property is simpler and only contains +the property’s key id in the column. The property id and the property +value are stored in the value. If the property key is defined as +`list()`, however, the property id is stored in the column as well. diff --git a/docs/advanced-topics/eventual-consistency.md b/docs/advanced-topics/eventual-consistency.md new file mode 100644 index 0000000000..2f7c73c674 --- /dev/null +++ b/docs/advanced-topics/eventual-consistency.md @@ -0,0 +1,197 @@ +Eventually-Consistent Storage Backends +====================================== + +When running JanusGraph against an eventually consistent storage backend +special JanusGraph features must be used to ensure data consistency and +special considerations must be made regarding data degradation. + +This page summarizes some of the aspects to consider when running +JanusGraph on top of an eventually consistent storage backend like +Apache Cassandra or Apache HBase. + +Data Consistency +---------------- + +On eventually consistent storage backends, JanusGraph must obtain locks +in order to ensure consistency because the underlying storage backend +does not provide transactional isolation. In the interest of efficiency, +JanusGraph does not use locking by default. Hence, the user has to +decide for each schema element that defines a consistency constraint +whether or not to use locking. Use `JanusGraphManagement.setConsistency(element, ConsistencyModifier.LOCK)` +to explicitly enable locking on a schema element as shown in the +following examples. +```groovy +mgmt = graph.openManagement() +name = mgmt.makePropertyKey('consistentName').dataType(String.class).make() +index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex() +mgmt.setConsistency(name, ConsistencyModifier.LOCK) // Ensures only one name per vertex +mgmt.setConsistency(index, ConsistencyModifier.LOCK) // Ensures name uniqueness in the graph +mgmt.commit() +``` + +When updating an element that is guarded by a uniqueness constraint, +JanusGraph uses the following protocol at the end of a transaction when +calling `tx.commit()`: + +1. Acquire a lock on all elements that have a consistency constraint +2. Re-read those elements from the storage backend and verify that they + match the state of the element in the current transaction prior to + modification. If not, the element was concurrently modified and a + PermanentLocking exception is thrown. +3. Persist the state of the transaction against the storage backend. +4. Release all locks. + +This is a brief description of the locking protocol which leaves out +optimizations (e.g. local conflict detection) and detection of failure +scenarios (e.g. expired locks). + +The actual lock application mechanism is abstracted such that JanusGraph +can use multiple implementations of a locking provider. Currently, two +locking providers are supported in the JanusGraph distribution: + +1. A locking implementation based on key-consistent read and write + operations that is agnostic to the underlying storage backend as + long as it supports key-consistent operations (which includes + Cassandra and HBase). This is the default implementation and uses + timestamp based lock applications to determine which transaction + holds the lock. +2. A Cassandra specific locking implementation based on the Astyanax + locking recipe. + +Both locking providers require that clocks are synchronized across all +machines in the cluster. + +!!! warning + The locking implementation is not robust against all failure + scenarios. For instance, when a Cassandra cluster drops below quorum, + consistency is no longer ensured. Hence, it is suggested to use + locking-based consistency constraints sparingly with eventually + consistent storage backends. For use cases that require strict and or + frequent consistency constraint enforcement, it is suggested to use a + storage backend that provides transactional isolation. + +### Data Consistency without Locks + +Because of the additional steps required to acquire a lock when +committing a modifying transaction, locking is a fairly expensive way to +ensure consistency and can lead to deadlock when very many concurrent +transactions try to modify the same elements in the graph. Hence, +locking should be used in situations where consistency is more important +than write latency and the number of conflicting transactions is small. + +In other situations, it may be better to allow conflicting transactions +to proceed and to resolve inconsistencies at read time. This is a design +pattern commonly employed in large scale data systems and most effective +when the actual likelihood of conflict is small. Hence, write +transactions don’t incur additional overhead and any (unlikely) conflict +that does occur is detected and resolved at read time and later cleaned +up. JanusGraph makes it easy to use this strategy through the following +features. + +#### Forking Edges + +Because edge are stored as single records in the underlying storage +backend, concurrently modifying a single edge would lead to conflict. +Instead of locking, an edge label can be configured to use +`ConsistencyModifier.FORK`. The following example creates a new edge +label `related` and defines its consistency to FORK. + +```groovy +mgmt = graph.openManagement() +related = mgmt.makeEdgeLabel('related').make() +mgmt.setConsistency(related, ConsistencyModifier.FORK) +mgmt.commit() +``` + +When modifying an edge whose label is configured to FORK the edge is +deleted and the modified edge is added as a new one. Hence, if two +concurrent transactions modify the same edge, two modified copies of the +edge will exist upon commit which can be resolved during querying +traversals if needed. + +!!! note + Edge forking only applies to MULTI edges. Edge labels with a + multiplicity constraint cannot use this strategy since a constraint is + built into the edge label definition that requires an explicit lock or + use the conflict resolution mechanism of the underlying storage + backend. + +#### Multi-Properties + +Modifying single valued properties on vertices concurrently can result +in a conflict. Similarly to edges, one can allow an arbitrary number of +properties on a vertex for a particular property key defined with +cardinality LIST and FORK on modification. Hence, instead of conflict +one reads multiple properties. Since JanusGraph allows properties on +properties, provenance information like `author` can be added to the +properties to facilitate resolution at read time. + +See [multi-properties](../basics/schema.md#property-key-cardinality) to learn how to define +those. + +Data Inconsistency +------------------ + +### Temporary Inconsistency + +On eventually consistent storage backends, writes may not be immediately +visible to the entire cluster causing temporary inconsistencies in the +graph. This is an inherent property of eventual consistency, in the +sense, that accepted updates must be propagated to other instances in +the cluster and no guarantees are made with respect to read atomicity in +the interest of performance. + +From JanusGraph’s perspective, eventual consistency might cause the +following temporary graph inconsistencies in addition the general +inconsistency that some parts of a transaction are visible while others +aren’t yet. + +**Stale Index entries** +Index entries might point to nonexistent vertices or edges. Similarly, a +vertex or edge appears in the graph but is not yet indexed and hence +ignored by global graph queries. + +**Half-Edges** +Only one direction of an edge gets persisted or deleted which might lead +to the edge not being or incorrectly being retrieved. + +!!! note + In order to avoid that write failures result in permanent + inconsistencies in the graph it is recommended to use storage backends + that support batch write atomicity and to ensure that write atomicity + is enabled. To get the benefit of write atomicity, the number + modifications made in a single transaction must be smaller than the + configured `buffer-size` option documented in [Configuration Reference](../basics/configuration-reference.md). The + buffer size defines the maximum number of modifications that + JanusGraph will persist in a single batch. If a transaction has more + modifications, the persistence will be split into multiple batches + which are persisted individually which is useful for batch loading but + invalidates write atomicity. + +### Ghost Vertices + +A permanent inconsistency that can arise when operating JanusGraph on +eventually consistent storage backend is the phenomena of **ghost +vertices**. If a vertex gets deleted while it is concurrently being +modified, the vertex might re-appear as a *ghost*. + +The following strategies can be used to mitigate this issue: + +**Existence checks** +Configure transactions to (double) check for the existence of vertices +prior to returning them. Please see [Transaction Configuration](../basics/transactions.md#transaction-configuration) for more +information and note that this can significantly decrease performance. +Note, that this does not fix the inconsistencies but hides some of them +from the user. + +**Regular Clean-ups** +Run regular batch-jobs to repair inconsistencies in the graph using +[JanusGraph with TinkerPop’s Hadoop-Gremlin](hadoop.md). +This is the only strategy that can address all +inconsistencies and effectively repair them. We will provide increasing +support for such repairs in future versions of Faunus. + +**Soft Deletes** +Instead of deleting vertices, they are marked as deleted which keeps +them in the graph for future analysis but hides them from user-facing +transactions. diff --git a/docs/hadoop.adoc b/docs/advanced-topics/hadoop.md similarity index 62% rename from docs/hadoop.adoc rename to docs/advanced-topics/hadoop.md index b880e6f6ca..980f82154c 100644 --- a/docs/hadoop.adoc +++ b/docs/advanced-topics/hadoop.md @@ -1,54 +1,82 @@ -[[hadoop-tp3]] -== JanusGraph with TinkerPop's Hadoop-Gremlin - -This chapter describes how to leverage https://hadoop.apache.org/[Apache Hadoop] and https://spark.apache.org/[Apache Spark] to configure JanusGraph for distributed graph processing. These steps will provide an overview on how to get started with those projects, but please refer to those project communities to become more deeply familiar with them. - -JanusGraph-Hadoop works with TinkerPop's https://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference/#hadoop-gremlin[hadoop-gremlin] package for -general-purpose OLAP. - -For the scope of the example below, Apache Spark is the computing framework and Apache Cassandra is the storage backend. The directions can be followed with other packages with minor changes to the configuration properties. - -[NOTE] -The examples in this chapter are based on running Spark in local mode or standalone cluster mode. Additional configuration -is required when using Spark on YARN or Mesos. - -=== Configuring Hadoop for Running OLAP -For running OLAP queries from the Gremlin Console, a few prerequisites need to be fulfilled. You will need to add the Hadoop configuration directory into the `CLASSPATH`, and the configuration directory needs to point to a live Hadoop cluster. - -Hadoop provides a distributed access-controlled file system. The Hadoop file system is used by Spark workers running on different machines to have a common source for file based operations. The intermediate computations of various OLAP queries may be persisted on the Hadoop file system. - -For configuring a single node Hadoop cluster, please refer to official https://hadoop.apache.org/docs/r$MAVEN{hadoop2.version}/hadoop-project-dist/hadoop-common/SingleCluster.html[Apache Hadoop Docs] - -Once you have a Hadoop cluster up and running, we will need to specify the Hadoop configuration files in the `CLASSPATH`. The below document expects that you have those configuration files located under `/etc/hadoop/conf`. - -Once verified, follow the below steps to add the Hadoop configuration to the `CLASSPATH` and start the Gremlin Console, which will play the role of the Spark driver program. - -[source, shell] ----- +JanusGraph with TinkerPop’s Hadoop-Gremlin +========================================== + +This chapter describes how to leverage [Apache Hadoop](https://hadoop.apache.org/) +and [Apache Spark](https://spark.apache.org/) to configure JanusGraph for +distributed graph processing. These steps will provide an overview on +how to get started with those projects, but please refer to those +project communities to become more deeply familiar with them. + +JanusGraph-Hadoop works with TinkerPop’s +[hadoop-gremlin](https://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/#hadoop-gremlin) +package for general-purpose OLAP. + +For the scope of the example below, Apache Spark is the computing +framework and Apache Cassandra is the storage backend. The directions +can be followed with other packages with minor changes to the +configuration properties. + +!!! note + The examples in this chapter are based on running Spark in local mode + or standalone cluster mode. Additional configuration is required when + using Spark on YARN or Mesos. + +Configuring Hadoop for Running OLAP +----------------------------------- + +For running OLAP queries from the Gremlin Console, a few prerequisites +need to be fulfilled. You will need to add the Hadoop configuration +directory into the `CLASSPATH`, and the configuration directory needs to +point to a live Hadoop cluster. + +Hadoop provides a distributed access-controlled file system. The Hadoop +file system is used by Spark workers running on different machines to +have a common source for file based operations. The intermediate +computations of various OLAP queries may be persisted on the Hadoop file +system. + +For configuring a single node Hadoop cluster, please refer to official +[Apache Hadoop Docs](https://hadoop.apache.org/docs/r{{hadoop2_version }}/hadoop-project-dist/hadoop-common/SingleCluster.html) + +Once you have a Hadoop cluster up and running, we will need to specify +the Hadoop configuration files in the `CLASSPATH`. The below document +expects that you have those configuration files located under +`/etc/hadoop/conf`. + +Once verified, follow the below steps to add the Hadoop configuration to +the `CLASSPATH` and start the Gremlin Console, which will play the role +of the Spark driver program. +```bash export HADOOP_CONF_DIR=/etc/hadoop/conf export CLASSPATH=$HADOOP_CONF_DIR bin/gremlin.sh ----- - -Once the path to Hadoop configuration has been added to the `CLASSPATH`, we can verify whether the Gremlin Console can access the Hadoop cluster by following these quick steps: +``` -[source, gremlin] ----- +Once the path to Hadoop configuration has been added to the `CLASSPATH`, +we can verify whether the Gremlin Console can access the Hadoop cluster +by following these quick steps: +```groovy gremlin> hdfs ==>storage[org.apache.hadoop.fs.LocalFileSystem@65bb9029] // BAD gremlin> hdfs ==>storage[DFS[DFSClient[clientName=DFSClient_NONMAPREDUCE_1229457199_1, ugi=user (auth:SIMPLE)]]] // GOOD ----- +``` +OLAP Traversals +--------------- -=== OLAP Traversals +JanusGraph-Hadoop works with TinkerPop’s hadoop-gremlin package for +general-purpose OLAP to traverse over the graph, and parallelize queries +by leveraging Apache Spark. -JanusGraph-Hadoop works with TinkerPop's hadoop-gremlin package for general-purpose OLAP to traverse over the graph, and parallelize queries by leveraging Apache Spark. +### OLAP Traversals with Spark Local -==== OLAP Traversals with Spark Local - -The backend demonstrated here is Cassandra for the OLAP example below. Additional configuration will be needed that is specific to that storage backend. The configuration is specified by the `gremlin.hadoop.graphReader` property which specifies the class to read data from the storage backend. +The backend demonstrated here is Cassandra for the OLAP example below. +Additional configuration will be needed that is specific to that storage +backend. The configuration is specified by the +`gremlin.hadoop.graphReader` property which specifies the class to read +data from the storage backend. JanusGraph currently supports following graphReader classes: @@ -56,10 +84,11 @@ JanusGraph currently supports following graphReader classes: * `CassandraInputFormat` for use with Cassandra 2 * `HBaseInputFormat` for use with HBase -The following properties file can be used to connect a JanusGraph instance in Cassandra such that it can be used with HadoopGraph to run OLAP queries. +The following properties file can be used to connect a JanusGraph +instance in Cassandra such that it can be used with HadoopGraph to run +OLAP queries. -[source, properties] ----- +```conf # read-cassandra-3.properties # # Hadoop Graph Configuration @@ -100,13 +129,11 @@ spark.master=local[*] spark.executor.memory=1g spark.serializer=org.apache.spark.serializer.KryoSerializer spark.kryo.registrator=org.apache.tinkerpop.gremlin.spark.structure.io.gryo.GryoRegistrator +``` ----- - -First create a properties file with above configurations, and load the same on the Gremlin Console to run OLAP queries as follows: - -[source, gremlin] ----- +First create a properties file with above configurations, and load the +same on the Gremlin Console to run OLAP queries as follows: +```bash bin/gremlin.sh \,,,/ @@ -130,23 +157,29 @@ gremlin> g.V().count() gremlin> g.E().count() ...... ==> 8046 ----- +``` -==== OLAP Traversals with Spark Standalone Cluster +### OLAP Traversals with Spark Standalone Cluster -The steps followed in the previous section can also be used with a Spark standalone cluster with only minor changes: +The steps followed in the previous section can also be used with a Spark +standalone cluster with only minor changes: -* Update the `spark.master` property to point to the Spark master URL instead of local -* Update the `spark.executor.extraClassPath` to enable the Spark executor to find the JanusGraph dependency jars -* Copy the JanusGraph dependency jars into the location specified in the previous step on each Spark executor machine +- Update the `spark.master` property to point to the Spark master URL + instead of local -[NOTE] -We have copied all the jars under *janusgraph-distribution/lib* into /opt/lib/janusgraph/ and the same directory structure is created across all workers, and jars are manually copied across all workers. +- Update the `spark.executor.extraClassPath` to enable the Spark + executor to find the JanusGraph dependency jars -The final properties file used for OLAP traversal is as follows: +- Copy the JanusGraph dependency jars into the location specified in + the previous step on each Spark executor machine + +!!! note + We have copied all the jars under **janusgraph-distribution/lib** into + /opt/lib/janusgraph/ and the same directory structure is created + across all workers, and jars are manually copied across all workers. -[source, properties] ----- +The final properties file used for OLAP traversal is as follows: +```conf # read-cassandra-3.properties # # Hadoop Graph Configuration @@ -188,12 +221,10 @@ spark.executor.memory=1g spark.executor.extraClassPath=/opt/lib/janusgraph/* spark.serializer=org.apache.spark.serializer.KryoSerializer spark.kryo.registrator=org.apache.tinkerpop.gremlin.spark.structure.io.gryo.GryoRegistrator ----- +``` Then use the properties file as follows from the Gremlin Console: - -[source, gremlin] ----- +```bash bin/gremlin.sh \,,,/ @@ -217,11 +248,19 @@ gremlin> g.V().count() gremlin> g.E().count() ...... ==> 8046 ----- - - -=== Other Vertex Programs - -Apache TinkerPop provides various vertex programs. A vertex program runs on each vertex until either a termination criteria is attained or a fixed number of iterations has been reached. Due to the parallel nature of vertex programs, they can leverage parallel computing frameworks like Spark or Giraph to improve their performance. - -Once you are familiar with how to configure JanusGraph to work with Spark, you can run all the other vertex programs provided by Apache TinkerPop, like Page Rank, Bulk Loading and Peer Pressure. See the http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference/#vertexprogram[TinkerPop VertexProgram docs] for more details. +``` + +Other Vertex Programs +--------------------- + +Apache TinkerPop provides various vertex programs. A vertex program runs +on each vertex until either a termination criteria is attained or a +fixed number of iterations has been reached. Due to the parallel nature +of vertex programs, they can leverage parallel computing frameworks like +Spark or Giraph to improve their performance. + +Once you are familiar with how to configure JanusGraph to work with +Spark, you can run all the other vertex programs provided by Apache +TinkerPop, like Page Rank, Bulk Loading and Peer Pressure. See the +[TinkerPop VertexProgram docs](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/#vertexprogram) +for more details. diff --git a/docs/advanced-topics/index-admin.md b/docs/advanced-topics/index-admin.md new file mode 100644 index 0000000000..479c980855 --- /dev/null +++ b/docs/advanced-topics/index-admin.md @@ -0,0 +1,500 @@ +Index Management +================ + +Reindexing +---------- + +[Graph Index](../basics/index-performance.md#graph-index) and [Vertex-centric Indexes](../basics/index-performance.md#vertex-centric-indexes) describe how to build +graph-global and vertex-centric indexes to improve query performance. +These indexes are immediately available if the indexed keys or labels +have been newly defined in the same management transaction. In this +case, there is no need to reindex the graph and this section can be +skipped. If the indexed keys and labels already existed prior to index +construction it is necessary to reindex the entire graph in order to +ensure that the index contains previously added elements. This section +describes the reindexing process. + +!!! warning + Reindexing is a manual process comprised of multiple steps. These + steps must be carefully followed in the right order to avoid index + inconsistencies. + +### Overview + +JanusGraph can begin writing incremental index updates right after an +index is defined. However, before the index is complete and usable, +JanusGraph must also take a one-time read pass over all existing graph +elements associated with the newly indexed schema type(s). Once this +reindexing job has completed, the index is fully populated and ready to +be used. The index must then be enabled to be used during query +processing. + +### Prior to Reindex + +The starting point of the reindexing process is the construction of an +index. Refer to [Indexing for Better Performance](../basics/index-performance.md) for a complete discussion of global +graph and vertex-centric indexes. Note, that a global graph index is +uniquely identified by its name. A vertex-centric index is uniquely +identified by the combination of its name and the edge label or property +key on which the index is defined - the name of the latter is referred +to as the **index type** in this section and only applies to +vertex-centric indexes. + +After building a new index against existing schema elements it is +recommended to wait a few minutes for the index to be announced to the +cluster. Note the index name (and the index type in case of a +vertex-centric index) since this information is needed when reindexing. + +### Preparing to Reindex + +There is a choice between two execution frameworks for reindex jobs: + +- MapReduce +- JanusGraphManagement + +Reindex on MapReduce supports large, horizontally-distributed databases. +Reindex on JanusGraphManagement spawns a single-machine OLAP job. This +is intended for convenience and speed on those databases small enough to +be handled by one machine. + +Reindexing requires: + +- The index name (a string — the user provides this to JanusGraph when + building a new index) +- The index type (a string — the name of the edge label or property + key on which the vertex-centric index is built). This applies only + to vertex-centric indexes - leave blank for global graph indexes. + +### Executing a Reindex Job on MapReduce + +The recommended way to generate and run a reindex job on MapReduce is +through the `MapReduceIndexManagement` class. Here is a rough outline of +the steps to run a reindex job using this class: + +- Open a `JanusGraph` instance +- Pass the graph instance into `MapReduceIndexManagement`'s constructor +- Call `updateIndex(, SchemaAction.REINDEX)` on the `MapReduceIndexManagement` instance +- If the index has not yet been enabled, enable it through `JanusGraphManagement` + +This class implements an `updateIndex` method that supports only the +`REINDEX` and `REMOVE_INDEX` actions for its `SchemaAction` parameter. +The class starts a Hadoop MapReduce job using the Hadoop configuration +and jars on the classpath. Both Hadoop 1 and 2 are supported. This class +gets metadata about the index and storage backend (e.g. the Cassandra +partitioner) from the `JanusGraph` instance given to its constructor. +```groovy +graph = JanusGraphFactory.open(...) +mgmt = graph.openManagement() +mr = new MapReduceIndexManagement(graph) +mr.updateIndex(mgmt.getRelationIndex(mgmt.getRelationType("battled"), "battlesByTime"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +#### Reindex Example on MapReduce + +The following Gremlin snippet outlines all steps of the MapReduce +reindex process in one self-contained example using minimal dummy data +against the Cassandra storage backend. +```groovy +// Open a graph +graph = JanusGraphFactory.open("conf/janusgraph-cql-es.properties") +g = graph.traversal() + +// Define a property +mgmt = graph.openManagement() +desc = mgmt.makePropertyKey("desc").dataType(String.class).make() +mgmt.commit() + +// Insert some data +graph.addVertex("desc", "foo bar") +graph.addVertex("desc", "foo baz") +graph.tx().commit() + +// Run a query -- note the planner warning recommending the use of an index +g.V().has("desc", containsText("baz")) + +// Create an index +mgmt = graph.openManagement() + +desc = mgmt.getPropertyKey("desc") +mixedIndex = mgmt.buildIndex("mixedExample", Vertex.class).addKey(desc).buildMixedIndex("search") +mgmt.commit() + +// Rollback or commit transactions on the graph which predate the index definition +graph.tx().rollback() + +// Block until the SchemaStatus transitions from INSTALLED to REGISTERED +report = ManagementSystem.awaitGraphIndexStatus(graph, "mixedExample").call() + +// Run a JanusGraph-Hadoop job to reindex +mgmt = graph.openManagement() +mr = new MapReduceIndexManagement(graph) +mr.updateIndex(mgmt.getGraphIndex("mixedExample"), SchemaAction.REINDEX).get() + +// Enable the index +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("mixedExample"), SchemaAction.ENABLE_INDEX).get() +mgmt.commit() + +// Block until the SchemaStatus is ENABLED +mgmt = graph.openManagement() +report = ManagementSystem.awaitGraphIndexStatus(graph, "mixedExample").status(SchemaStatus.ENABLED).call() +mgmt.rollback() + +// Run a query -- JanusGraph will use the new index, no planner warning +g.V().has("desc", containsText("baz")) + +// Concerned that JanusGraph could have read cache in that last query, instead of relying on the index? +// Start a new instance to rule out cache hits. Now we're definitely using the index. +graph.close() +graph = JanusGraphFactory.open("conf/janusgraph-cql-es.properties") +g.V().has("desc", containsText("baz")) +``` + +### Executing a Reindex job on JanusGraphManagement + +To run a reindex job on JanusGraphManagement, invoke +`JanusGraphManagement.updateIndex` with the `SchemaAction.REINDEX` +argument. For example: +```groovy +m = graph.openManagement() +i = m.getGraphIndex('indexName') +m.updateIndex(i, SchemaAction.REINDEX).get() +m.commit() +``` + +#### Example for JanusGraphManagement + +The following loads some sample data into a BerkeleyDB-backed JanusGraph +database, defines an index after the fact, reindexes using +JanusGraphManagement, and finally enables and uses the index: +```groovy +import org.janusgraph.graphdb.database.management.ManagementSystem + +// Load some data from a file without any predefined schema +graph = JanusGraphFactory.open('conf/janusgraph-berkeleyje.properties') +g = graph.traversal() +m = graph.openManagement() +m.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.LIST).make() +m.makePropertyKey('lang').dataType(String.class).cardinality(Cardinality.LIST).make() +m.makePropertyKey('age').dataType(Integer.class).cardinality(Cardinality.LIST).make() +m.commit() +graph.io(IoCore.gryo()).readGraph('data/tinkerpop-modern.gio') +graph.tx().commit() + +// Run a query -- note the planner warning recommending the use of an index +g.V().has('name', 'lop') +graph.tx().rollback() + +// Create an index +m = graph.openManagement() +m.buildIndex('names', Vertex.class).addKey(m.getPropertyKey('name')).buildCompositeIndex() +m.commit() +graph.tx().commit() + +// Block until the SchemaStatus transitions from INSTALLED to REGISTERED +ManagementSystem.awaitGraphIndexStatus(graph, 'names').status(SchemaStatus.REGISTERED).call() + +// Reindex using JanusGraphManagement +m = graph.openManagement() +i = m.getGraphIndex('names') +m.updateIndex(i, SchemaAction.REINDEX) +m.commit() + +// Enable the index +ManagementSystem.awaitGraphIndexStatus(graph, 'names').status(SchemaStatus.ENABLED).call() + +// Run a query -- JanusGraph will use the new index, no planner warning +g.V().has('name', 'lop') +graph.tx().rollback() + +// Concerned that JanusGraph could have read cache in that last query, instead of relying on the index? +// Start a new instance to rule out cache hits. Now we're definitely using the index. +graph.close() +graph = JanusGraphFactory.open("conf/janusgraph-berkeleyje.properties") +g = graph.traversal() +g.V().has('name', 'lop') +``` + +Index Removal +------------- + +!!! warning + Index removal is a manual process comprised of multiple steps. These + steps must be carefully followed in the right order to avoid index + inconsistencies. + +### Overview + +Index removal is a two-stage process. In the first stage, one JanusGraph +signals to all others via the storage backend that the index is slated +for deletion. This changes the index’s state to `DISABLED`. At that +point, JanusGraph stops using the index to answer queries and stops +incrementally updating the index. Index-related data in the storage +backend remains present but ignored. + +The second stage depends on whether the index is mixed or composite. A +composite index can be deleted via JanusGraph. As with reindexing, +removal can be done through either MapReduce or JanusGraphManagement. +However, a mixed index must be manually dropped in the index backend; +JanusGraph does not provide an automated mechanism to delete an index +from its index backend. + +Index removal deletes everything associated with the index except its +schema definition and its `DISABLED` state. This schema stub for the +index remains even after deletion, though its storage footprint is +negligible and fixed. + +### Preparing for Index Removal + +If the index is currently enabled, it should first be disabled. This is +done through the `ManagementSystem`. +```groovy +mgmt = graph.openManagement() +rindex = mgmt.getRelationIndex(mgmt.getRelationType("battled"), "battlesByTime") +mgmt.updateIndex(rindex, SchemaAction.DISABLE_INDEX).get() +gindex = mgmt.getGraphIndex("byName") +mgmt.updateIndex(gindex, SchemaAction.DISABLE_INDEX).get() +mgmt.commit() +``` + +Once the status of all keys on the index changes to `DISABLED`, the +index is ready to be removed. A utility in ManagementSystem can automate +the wait-for-`DISABLED` step: +```groovy +ManagementSystem.awaitGraphIndexStatus(graph, 'byName').status(SchemaStatus.DISABLED).call() +``` + +After a composite index is `DISABLED`, there is a choice between two +execution frameworks for its removal: + +- MapReduce +- JanusGraphManagement + +Index removal on MapReduce supports large, horizontally-distributed +databases. Index removal on JanusGraphManagement spawns a single-machine +OLAP job. This is intended for convenience and speed on those databases +small enough to be handled by one machine. + +Index removal requires: + +- The index name (a string — the user provides this to JanusGraph when + building a new index) +- The index type (a string — the name of the edge label or property + key on which the vertex-centric index is built). This applies only + to vertex-centric indexes - leave blank for global graph indexes. + +As noted in the overview, a mixed index must be manually dropped from +the indexing backend. Neither the MapReduce framework nor the +JanusGraphManagement framework will delete a mixed backend from the +indexing backend. + +### Executing an Index Removal Job on MapReduce + +As with reindexing, the recommended way to generate and run an index +removal job on MapReduce is through the `MapReduceIndexManagement` +class. Here is a rough outline of the steps to run an index removal job +using this class: + +- Open a `JanusGraph` instance +- If the index has not yet been disabled, disable it through `JanusGraphManagement` +- Pass the graph instance into `MapReduceIndexManagement`'s constructor +- Call `updateIndex(, SchemaAction.REMOVE_INDEX)` + +A commented code example follows in the next subsection. + +#### Example for MapReduce + +```groovy +import org.janusgraph.graphdb.database.management.ManagementSystem + +// Load the "Graph of the Gods" sample data +graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties') +g = graph.traversal() +GraphOfTheGodsFactory.load(graph) + +g.V().has('name', 'jupiter') + +// Disable the "name" composite index +m = graph.openManagement() +nameIndex = m.getGraphIndex('name') +m.updateIndex(nameIndex, SchemaAction.DISABLE_INDEX).get() +m.commit() +graph.tx().commit() + +// Block until the SchemaStatus transitions from INSTALLED to REGISTERED +ManagementSystem.awaitGraphIndexStatus(graph, 'name').status(SchemaStatus.DISABLED).call() + +// Delete the index using MapReduceIndexJobs +m = graph.openManagement() +mr = new MapReduceIndexManagement(graph) +future = mr.updateIndex(m.getGraphIndex('name'), SchemaAction.REMOVE_INDEX) +m.commit() +graph.tx().commit() +future.get() + +// Index still shows up in management interface as DISABLED -- this is normal +m = graph.openManagement() +idx = m.getGraphIndex('name') +idx.getIndexStatus(m.getPropertyKey('name')) +m.rollback() + +// JanusGraph should issue a warning about this query requiring a full scan +g.V().has('name', 'jupiter') +``` + +### Executing an Index Removal job on JanusGraphManagement + +To run an index removal job on JanusGraphManagement, invoke +`JanusGraphManagement.updateIndex` with the `SchemaAction.REMOVE_INDEX` +argument. For example: +```groovy +m = graph.openManagement() +i = m.getGraphIndex('indexName') +m.updateIndex(i, SchemaAction.REMOVE_INDEX).get() +m.commit() +``` + +#### Example for JanusGraphManagement + +The following loads some indexed sample data into a BerkeleyDB-backed +JanusGraph database, then disables and removes the index through +JanusGraphManagement: +```groovy +import org.janusgraph.graphdb.database.management.ManagementSystem + +// Load the "Graph of the Gods" sample data +graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties') +g = graph.traversal() +GraphOfTheGodsFactory.load(graph) + +g.V().has('name', 'jupiter') + +// Disable the "name" composite index +m = graph.openManagement() +nameIndex = m.getGraphIndex('name') +m.updateIndex(nameIndex, SchemaAction.DISABLE_INDEX).get() +m.commit() +graph.tx().commit() + +// Block until the SchemaStatus transitions from INSTALLED to REGISTERED +ManagementSystem.awaitGraphIndexStatus(graph, 'name').status(SchemaStatus.DISABLED).call() + +// Delete the index using JanusGraphManagement +m = graph.openManagement() +nameIndex = m.getGraphIndex('name') +future = m.updateIndex(nameIndex, SchemaAction.REMOVE_INDEX) +m.commit() +graph.tx().commit() + +future.get() + +m = graph.openManagement() +nameIndex = m.getGraphIndex('name') + +g.V().has('name', 'jupiter') +``` + +Common Problems with Index Management +------------------------------------- + +### IllegalArgumentException when starting job + +When a reindexing job is started shortly after a the index has been +built, the job might fail with an exception like one of the following: + + The index mixedExample is in an invalid state and cannot be indexed. + The following index keys have invalid status: desc has status INSTALLED + (status must be one of [REGISTERED, ENABLED]) + + The index mixedExample is in an invalid state and cannot be indexed. + The index has status INSTALLED, but one of [REGISTERED, ENABLED] is required + +When an index is built, its existence is broadcast to all other +JanusGraph instances in the cluster. Those must acknowledge the +existence of the index before the reindexing process can be started. The +acknowledgments can take a while to come in depending on the size of the +cluster and the connection speed. Hence, one should wait a few minutes +after building the index and before starting the reindex process. + +Note, that the acknowledgment might fail due to JanusGraph instance +failure. In other words, the cluster might wait indefinitely on the +acknowledgment of a failed instance. In this case, the user must +manually remove the failed instance from the cluster registry as +described in [Failure & Recovery](recovery.md). After the cluster state has been +restored, the acknowledgment process must be reinitiated by manually +registering the index again in the management system. + +```groovy +mgmt = graph.openManagement() +rindex = mgmt.getRelationIndex(mgmt.getRelationType("battled"),"battlesByTime") +mgmt.updateIndex(rindex, SchemaAction.REGISTER_INDEX).get() +gindex = mgmt.getGraphIndex("byName") +mgmt.updateIndex(gindex, SchemaAction.REGISTER_INDEX).get() +mgmt.commit() +``` + +After waiting a few minutes for the acknowledgment to arrive the reindex +job should start successfully. + +### Could not find index + +This exception in the reindexing job indicates that an index with the +given name does not exist or that the name has not been specified +correctly. When reindexing a global graph index, only the name of the +index as defined when building the index should be specified. When +reindexing a global graph index, the name of the index must be given in +addition to the name of the edge label or property key on which the +vertex-centric index is defined. + +### Cassandra Mappers Fail with "Too many open files" + +The end of the exception stacktrace may look like this: +```java +java.net.SocketException: Too many open files + at java.net.Socket.createImpl(Socket.java:447) + at java.net.Socket.getImpl(Socket.java:510) + at java.net.Socket.setSoLinger(Socket.java:988) + at org.apache.thrift.transport.TSocket.initSocket(TSocket.java:118) + at org.apache.thrift.transport.TSocket.(TSocket.java:109) +``` + +When running Cassandra with virtual nodes enabled, the number of virtual +nodes seems to set a floor under the number of mappers. Cassandra may +generate more mappers than virtual nodes for clusters with lots of data, +but it seems to generate at least as many mappers as there are virtual +nodes even though the cluster might be empty or close to empty. The +default is 256 as of this writing. + +Each mapper opens and quickly closes several sockets to Cassandra. The +kernel on the client side of those closed sockets goes into asynchronous +TIME\_WAIT, since Thrift uses SO\_LINGER. Only a small number of sockets +are open at any one time — usually low single digits — but potentially +many lingering sockets can accumulate in TIME\_WAIT. This accumulation +is most pronounced when running a reindex job locally (not on a +distributed MapReduce cluster), since all of those client-side +TIME\_WAIT sockets are lingering on a single client machine instead of +being spread out across many machines in a cluster. Combined with the +floor of 256 mappers, a reindex job can open thousands of sockets of the +course of its execution. When these sockets all linger in TIME\_WAIT on +the same client, they have the potential to reach the open-files ulimit, +which also controls the number of open sockets. The open-files ulimit is +often set to 1024. + +Here are a few suggestions for dealing with the "Too many open files" +problem during reindexing on a single machine: + +- Reduce the maximum size of the Cassandra connection pool. For + example, consider setting the cassandrathrift storage backend’s + `max-active` and `max-idle` options to 1 each, and setting + `max-total` to -1. See [Configuration Reference](../basics/configuration-reference.md) for full listings of + connection pool settings on the Cassandra storage backends. + +- Increase the `nofile` ulimit. The ideal value depends on the size of + the Cassandra dataset and the throughput of the reindex mappers; if + starting at 1024, try an order of magnitude larger: 10000. This is + just necessary to sustain lingering TIME\_WAIT sockets. The reindex + job won’t try to open nearly that many sockets at once. + +- Run the reindex task on a multi-node MapReduce cluster to spread out + the socket load. diff --git a/docs/JanusGraphBus.md b/docs/advanced-topics/janusgraph-bus.md similarity index 95% rename from docs/JanusGraphBus.md rename to docs/advanced-topics/janusgraph-bus.md index 7fd2ab45aa..c940fe0bc4 100644 --- a/docs/JanusGraphBus.md +++ b/docs/advanced-topics/janusgraph-bus.md @@ -1,3 +1,5 @@ +# JanusGraph Bus + The **JanusGraph Bus** describes a collection of configurable logs to which JanusGraph writes changes to the graph and its management. The JanusGraph Bus is used for internal (i.e. between multiple JanusGraph instances) and external (i.e. integration with other systems) communication. In particular, JanusGraph maintains three separate logs: @@ -6,8 +8,9 @@ In particular, JanusGraph maintains three separate logs: The purpose of the trigger log is to capture the mutations of a transaction so that the resulting changes to the graph can trigger events in other system. Such events may be propagating the change to other data stores, view maintenance, or aggregate computation. The trigger log consists of multiple sub-logs as configured by the user. When opening a transaction, the identifier for the trigger sub-log can be specified: - - tx = g.buildTransaction().logIdentifier("purchase").start(); +```java +tx = g.buildTransaction().logIdentifier("purchase").start(); +``` In this case, the identifier is "purchase" which means that the mutations of this transaction will be written to a log with the name "trigger_purchase". This gives the user control over where transactional mutations are logged. If no trigger log is specified, no trigger log entry will be created. diff --git a/docs/advanced-topics/migrating.md b/docs/advanced-topics/migrating.md new file mode 100644 index 0000000000..671ff82a6a --- /dev/null +++ b/docs/advanced-topics/migrating.md @@ -0,0 +1,63 @@ +Migrating from Titan +==================== + +This page describes some of the Configuration options that JanusGraph +provides to allow migration of data from a data store which had +previously been created by Titan. + +Configuration +------------- + +When connecting to an existing Titan data store the +`graph.titan-version` property should already be set in the global +configuration to Titan version `1.0.0`. The ID store name in JanusGraph +is configurable via the `ids.store-name` property whereas in Titan it +was a constant. If the `graph.titan-version` has been set in the +existing global configuration, then you do **not** need to explicitly +set the ID store as it will default to `titan_ids`. + +Cassandra +--------- + +The default keyspace used by Titan was `titan` and in order to reuse +that existing keyspace the `storage.cassandra.keyspace` property needs +to be set accordingly. +```conf +storage.cassandra.keyspace=titan +``` + +These configuration options allow JanusGraph to read data from a +Cassandra database which had previously been created by Titan. However, +once JanusGraph writes back to that database it will register additional +serializers which mean that it will no longer be compatible with Titan. +Users are therefore encouraged to backup the data in Casssandra before +attempting to use it with the JanusGraph release. + +HBase +----- + +The name of the table used by Titan was `titan` and in order to reuse +that existing table the `storage.hbase.table` property needs to be set +accordingly. +```conf +storage.hbase.table=titan +``` + +These configuration options allow JanusGraph to read data from an HBase +database which had previously been created by Titan. However, once +JanusGraph writes back to that database it will register additional +serializers which mean that it will no longer be compatible with Titan. +Users are therefore encouraged to backup the data in HBase before +attempting to use it with the JanusGraph release. + +BerkeleyDB +---------- + +The BerkeleyDB version has been updated, and it contains changes to the +file format stored on disk. This file format change is forward +compatible with previous versions of BerkeleyDB, so existing graph data +stored with Titan can be read in. However, once the data has been read +in with the newer version of BerkeleyDB, those files can no longer be +read by the older version. Users are encouraged to backup the BerkeleyDB +storage directory before attempting to use it with the JanusGraph +release. diff --git a/docs/advanced-topics/monitoring.md b/docs/advanced-topics/monitoring.md new file mode 100644 index 0000000000..6138255e26 --- /dev/null +++ b/docs/advanced-topics/monitoring.md @@ -0,0 +1,404 @@ +Monitoring JanusGraph +===================== + +Metrics in JanusGraph +--------------------- + +JanusGraph supports [Metrics](http://dropwizard.io/). JanusGraph can +measure the following: + +- The number of transactions begun, committed, and rolled back + +- The number of attempts and failures of each storage backend + operation type + +- The response time distribution of each storage backend operation + type + +### Configuring Metrics Collection + +To enable Metrics collection, set the following in JanusGraph’s +properties file: +```conf +# Required to enable Metrics in JanusGraph +metrics.enabled = true +``` + +This setting makes JanusGraph record measurements at runtime using +Metrics classes like Timer, Counter, Histogram, etc. To access these +measurements, one or more Metrics reporters must be configured as +described in the section [Configuring Metrics Reporting](#configuring-metrics-reporting). + +#### Customizing the Default Metric Names + +JanusGraph prefixes all metric names with "org.janusgraph" by default. +This prefix can be set through the `metrics.prefix` configuration +property. For example, to shorten the default "org.janusgraph" prefix to +just "janusgraph": +```conf +# Optional +metrics.prefix = janusgraph +``` + +#### Transaction-Specific Metrics Names + +Each JanusGraph transaction may optionally specify its own Metrics name +prefix, overriding both the default Metrics name prefix and the +`metrics.prefix` configuration property. For example, the prefix could +be changed to the name of the frontend application that opened the +JanusGraph transaction. Note that Metrics maintains a ConcurrentHashMap +of metric names and their associated objects in memory, so it’s probably +a good idea to keep the number of distinct metric prefixes small. + +To do this, call `TransactionBuilder.setMetricsPrefix(String)`: +```java +JanusGraph graph = ...; +TransactionBuilder tbuilder = graph.buildTransaction(); +JanusGraphTransaction tx = tbuilder.groupName("foobar").start(); +``` + +#### Separating Metrics by Backend Store + +JanusGraph combines the Metrics for its various internal storage backend +handles by default. All Metrics for storage backend interactions follow +the pattern "<prefix>.stores.<opname>", regardless of +whether they come from the ID store, edge store, etc. When +`metrics.merge-basic-metrics = false` is set in JanusGraph’s properties +file, the "stores" string in metric names is replaced by "idStore", +"edgeStore", "vertexIndexStore", or "edgeIndexStore". + +Configuring Metrics Reporting +----------------------------- + +JanusGraph supports the following Metrics reporters: + +- [Console](#console-reporter) +- [CSV](#csv-file-reporter) +- [Ganglia](#ganglia-reporter) +- [Graphite](#graphite-reporter) +- [JMX](#jmx-reporter) +- [Slf4j](#slf4j-reporter) +- [User-provided/Custom](#custom-reporter) + +Each reporter type is independent of and can coexist with the others. +For example, it’s possible to configure Ganglia, JMX, and Slf4j Metrics +reporters to operate simultaneously. Just set all their respective +configuration keys in janusgraph.properties (and enable metrics as +directed above). + +### Console Reporter + + + + + + + + + + + + + + + + + + + +
Metrics Console Reporter Configuration Options
Config KeyRequired?ValueDefault
metrics.console.intervalyesMilliseconds to wait between dumping metrics to the consolenull
+ +Example janusgraph.properties snippet that prints metrics to the console +once a minute: +```conf +metrics.enabled = true +# Required; specify logging interval in milliseconds +metrics.console.interval = 60000 +``` + +### CSV File Reporter + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrics CSV Reporter Configuration Options
Config KeyRequired?ValueDefault
metrics.csv.intervalyesMilliseconds to wait between writing CSV linesnull
metrics.csv.directoryyesDirectory in which CSV files are written (will be created if it does not exist)null
+ +Example janusgraph.properties snippet that writes CSV files once a +minute to the directory `./foo/bar/` (relative to the process’s working +directory): +```conf +metrics.enabled = true +# Required; specify logging interval in milliseconds +metrics.csv.interval = 60000 +metrics.csv.directory = foo/bar +``` + +### Ganglia Reporter + +!!! note + Configuration of [Ganglia](http://ganglia.sourceforge.net/) requires + an additional library that is not packaged with JanusGraph due to its + LGPL licensing that conflicts with the JanusGraph’s Apache 2.0 + License. To run with Ganglia monitoring, download the + `org.acplt:oncrpc` jar from + [here](http://repo1.maven.org/maven2/org/acplt/oncrpc/1.0.7/) and copy + it to the JanusGraph `/lib` directory before starting the server. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrics Ganglia Reporter Configuration Options
Config KeyRequired?ValueDefault
metrics.ganglia.hostnameyesUnicast host or multicast group to which our Metrics are sentnull
metrics.ganglia.intervalyesMilliseconds to wait between sending datagramsnull
metrics.ganglia.portnoUDP port to which we send Metrics datagrams8649
metrics.ganglia.addressing-modenoMust be "unicast" or "multicast"unicast
metrics.ganglia.ttlnoMulticast datagram TTL; ignore for unicast1
metrics.ganglia.protocol-31noBoolean; true to use Ganglia protocol 3.1, false to use 3.0true
metrics.ganglia.uuidnoHost UUID to report instead of IP:hostnamenull
metrics.ganglia.spoofnoOverride IP:hostname reported to Ganglianull
+ +Example janusgraph.properties snippet that sends unicast UDP datagrams +to localhost on the default port once every 30 seconds: +```conf +metrics.enabled = true +# Required; IP or hostname string +metrics.ganglia.hostname = 127.0.0.1 +# Required; specify logging interval in milliseconds +metrics.ganglia.interval = 30000 +``` + +Example janusgraph.properties snippet that sends unicast UDP datagrams +to a non-default destination port and which also spoofs the IP and +hostname reported to Ganglia: +```conf +metrics.enabled = true +# Required; IP or hostname string +metrics.ganglia.hostname = 1.2.3.4 +# Required; specify logging interval in milliseconds +metrics.ganglia.interval = 60000 +# Optional +metrics.ganglia.port = 6789 +metrics.ganglia.spoof = 10.0.0.1:zombo.com +``` + +### Graphite Reporter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrics Graphite Reporter Configuration Options
Config KeyRequired?ValueDefault
metrics.graphite.hostnameyesIP address or hostname to which Graphite plaintext protocol data are sentnull
metrics.graphite.intervalyesMilliseconds to wait between pushing data to Graphitenull
metrics.graphite.portnoPort to which Graphite plaintext protocol reports are sent2003
metrics.graphite.prefixnoArbitrary string prepended to all metric names sent to Graphitenull
+ +Example janusgraph.properties snippet that sends metrics to a Graphite +server on 192.168.0.1 every minute: +```conf +metrics.enabled = true +# Required; IP or hostname string +metrics.graphite.hostname = 192.168.0.1 +# Required; specify logging interval in milliseconds +metrics.graphite.interval = 60000 +``` + +### JMX Reporter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrics JMX Reporter Configuration Options
Config KeyRequired?ValueDefault
metrics.jmx.enabledyesBooleanfalse
metrics.jmx.domainnoMetrics will appear in this JMX domainMetrics’s own default
metrics.jmx.agentidnoMetrics will be reported with this JMX agent IDMetrics’s own default
+ +Example janusgraph.properties snippet: +```conf +metrics.enabled = true +# Required +metrics.jmx.enabled = true +# Optional; if omitted, then Metrics uses its default values +metrics.jmx.domain = foo +metrics.jmx.agentid = baz +``` + +### Slf4j Reporter + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrics Slf4j Reporter Configuration Options
Config KeyRequired?ValueDefault
metrics.slf4j.intervalyesMilliseconds to wait between dumping metrics to the loggernull
metrics.slf4j.loggernoSlf4j logger name to use"metrics"
+ +Example janusgraph.properties snippet that logs metrics once a minute to +the logger named `foo`: +```conf +metrics.enabled = true +# Required; specify logging interval in milliseconds +metrics.slf4j.interval = 60000 +# Optional; uses Metrics default when unset +metrics.slf4j.logger = foo +``` + +### User-Provided/Custom Reporter + +In case the Metrics reporter configuration options listed above are +insufficient, JanusGraph provides a utility method to access the single +`MetricRegistry` instance which holds all of its measurements. +```java +com.codahale.metrics.MetricRegistry janusgraphRegistry = + org.janusgraph.util.stats.MetricManager.INSTANCE.getRegistry(); +``` + +Code that accesses `janusgraphRegistry` this way can then attach +non-standard reporter types or standard reporter types with exotic +configurations to `janusgraphRegistry`. This approach is also useful if +the surrounding application already has a framework for Metrics reporter +configuration, or if the application needs multiple +differently-configured instances of one of JanusGraph’s supported +reporter types. For instance, one could use this approach to setup +multiple unicast Graphite reporters whereas JanusGraph’s properties +configuration is limited to just one Graphite reporter. diff --git a/docs/advanced-topics/partitioning.md b/docs/advanced-topics/partitioning.md new file mode 100644 index 0000000000..7bdc79db03 --- /dev/null +++ b/docs/advanced-topics/partitioning.md @@ -0,0 +1,108 @@ +Graph Partitioning +================== + +When JanusGraph is deployed on a cluster of multiple storage backend instances, the graph is partitioned across those machines. Since JanusGraph stores the graph in an adjacency list representation the assignment of vertices to machines determines the partitioning. By default, JanusGraph uses a random partitioning strategy that randomly assigns vertices to machines. Random partitioning is very efficient, requires no configuration, and results in balanced partitions. However, random partitioning results in less efficient query processing as the JanusGraph cluster grows to accommodate more graph data because of the increasing cross-instance communication required to retrieve the query's result set. Explicit graph partitioning can ensure that strongly connected and frequently traversed subgraphs are stored on the same instance thereby reducing the communication overhead significantly. + +To enable explicit graph partitioning in JanusGraph, the following configuration options must be set when the JanusGraph cluster is initialized. + +```conf +cluster.partition = true +cluster.max-partitions = 32 +ids.flush = false +``` + +The configuration option `max-partitions` controls how many virtual partitions JanusGraph creates. This number should be roughly twice the number of storage backend instances. If the cluster of storage backend instances is expected to grow, estimate the size of the cluster in the foreseeable future and take this number as the baseline. Setting this number too large will unnecessarily fragment the cluster which can lead to poor performance. + +Because explicit graph partitioning controls the assignment of vertices to storage instances it cannot be enabled once a JanusGraph cluster is initialized. Likewise, the number of virtual partitions cannot be changed without reloading the graph. + +Explicit graph partitioning can only enabled against storage backends that support ordered key storage: + +* *HBase*: Always supports explicit graph partitioning +* *Cassandra*: Must be configured to use *ByteOrderedPartitioner* in order to support explicit graph partitioning + +There are two aspects to graph partitioning which can be individually +controlled: edge cuts and vertex cuts. + +Edge Cut +-------- + +In assigning vertices to partitions one strives to optimize the +assignment such that frequently co-traversed vertices are hosted on the +same machine. Assume vertex A is assigned to machine 1 and vertex B is +assigned to machine 2. An edge between the vertices is called a **cut +edge** because its end points are hosted on separate machines. +Traversing this edge as part of a graph query requires communication +between the machines which slows down query processing. Hence, it is +desirable to reduce the edge cut for frequently traversed edges. That, +in turn, requires placing the adjacent vertices of frequently traversed +edges in the same partition. + +Vertices are placed in a partition by way of the assigned vertex id. A +partition is essentially a sequential range of vertex ids. To place a +vertex in a particular partition, JanusGraph chooses an id from the +partition’s range of vertex ids. JanusGraph controls the +vertex-to-partition assignment through the configured placement +strategy. By default, vertices created in the same transaction are +assigned to the same partition. This strategy is easy to reason about +and works well in situations where frequently co-traversed vertices are +created in the same transaction - either by optimizing the loading +strategy to that effect or because vertices are naturally added to the +graph that way. However, the strategy is limited, leads to imbalanced +partitions when data is loaded in large transactions and not the optimal +strategy for many use cases. The user can provide a use case specific +vertex placement strategy by implementing the `IDPlacementStrategy` +interface and registering it in the configuration through the +`ids.placement` option. + +When implementing `IDPlacementStrategy`, note that partitions are +identified by an integer id in the range from 0 to the number of +configured virtual partitions minus 1. For our example configuration, +there are partitions 0, 1, 2, 3, ..31. Partition ids are not the same as +vertex ids. Edge cuts are more meaningful when the JanusGraph servers +are on the same hosts as the storage backend. If you have to make a +network call to a different host on each hop of a traversal, the benefit +of edge cuts and custom placement strategies can be largely nullified. + +Vertex Cut +---------- + +While edge cut optimization aims to reduce the cross communication and +thereby improve query execution, vertex cuts address the hotspot issue +caused by vertices with a large number of incident edges. While +[vertex-centric indexes](../basics/index-performance.md#vertex-centric-indexes) effectively address query +performance for large degree vertices, vertex cuts are needed to address +the hot spot issue on very large graphs. + +Cutting a vertex means storing a subset of that vertex’s adjacency list +on each partition in the graph. In other words, the vertex and its +adjacency list is partitioned thereby effectively distributing the load +on that single vertex across all of the instances in the cluster and +removing the hot spot. + +JanusGraph cuts vertices by label. A vertex label can be defined as +*partitioned* which means that all vertices of that label will be +partitioned across the cluster in the manner described above. +```groovy +mgmt = graph.openManagement() +mgmt.makeVertexLabel('user').make() +mgmt.makeVertexLabel('product').partition().make() +mgmt.commit() +``` + +In the example above, `product` is defined as a partitioned vertex label +whereas `user` is a normal label. This configuration is beneficial for +situations where there are thousands of products but millions of users +and one records transactions between users and products. In that case, +the product vertices will have a very high degree and the popular +products turns into hot spots if they are not partitioned. + +Graph Partitioning FAQ +---------------------- + +### Random vs. Explicit Partitioning + +When the graph is small or accommodated by a few storage instances, it +is best to use random partitioning for its simplicity. As a rule of +thumb, one should strongly consider enabling explicit graph partitioning +and configure a suitable partitioning heuristic when the graph grows +into the 10s of billions of edges. diff --git a/docs/advanced-topics/recovery.md b/docs/advanced-topics/recovery.md new file mode 100644 index 0000000000..41dbc803cd --- /dev/null +++ b/docs/advanced-topics/recovery.md @@ -0,0 +1,103 @@ +Failure & Recovery +================== + +JanusGraph is a highly available and robust graph database. In large +scale JanusGraph deployments failure is inevitable. This page describes +some failure situations and how JanusGraph can handle them. + +Transaction Failure +------------------- + +Transactions can fail for a number of reasons. If the transaction fails +before the commit the changes will be discarded and the application can +retry the transaction in coherence with the business logic. Likewise, +locking or other consistency failures will cause an exception prior to +persistence and hence can be retried. The persistence stage of a +transaction is when JanusGraph starts persisting data to the various +backend systems. + +JanusGraph first persists all graph mutations to the storage backend. +This persistence is executed as one batch mutation to ensure that the +mutation is committed atomically for those backends supporting +atomicity. If the batch mutation fails due to an exception in the +storage backend, the entire transaction is failed. + +If the primary persistence into the storage backend succeeds but +secondary persistence into the indexing backends or the logging system +fail, the transaction is still considered to be successful because the +storage backend is the authoritative source of the graph. + +However, this can create inconsistencies with the indexes and logs. To +automatically repair such inconsistencies, JanusGraph can maintain a +transaction write-ahead log which is enabled through the configuration. +```conf +tx.log-tx = true +tx.max-commit-time = 10000 +``` + +The max-commit-time property is used to determine when a transaction has +failed. If the persistence stage of the transaction takes longer than +this time, JanusGraph will attempt to recover it if necessary. Hence, +this time out should be configured as a generous upper bound on the +maximum duration of persistence. Note, that this does not include the +time spent before commit. + +In addition, a separate process must be setup that reads the log to +identify partially failed transaction and repair any inconsistencies +caused. It is suggested to run the transaction repair process on a +separate machine connected to the cluster to isolate failures. Configure +a separately controlled process to run the following where the start +time specifies the time since epoch where the recovery process should +start reading from the write-ahead log. +```groovy +recovery = JanusGraphFactory.startTransactionRecovery(graph, startTime, TimeUnit.MILLISECONDS); +``` + +Enabling the transaction write-ahead log causes an additional write +operation for mutating transactions which increases the latency. Also +note, that additional space is required to store the log. The +transaction write-ahead log has a configurable time-to-live of 2 days +which means that log entries expire after that time to keep the storage +overhead small. Refer to [Configuration Reference](../basics/configuration-reference.md) for a complete list of all +log related configuration options to fine tune logging behavior. + +JanusGraph Instance Failure +--------------------------- + +JanusGraph is robust against individual instance failure in that other +instances of the JanusGraph cluster are not impacted by such failure and +can continue processing transactions without loss of performance while +the failed instance is restarted. + +However, some schema related operations - such as installing indexes - +require the coordination of all JanusGraph instances. For this reason, +JanusGraph maintains a record of all running instances. If an instance +fails, i.e. is not properly shut down, JanusGraph considers it to be +active and expects its participation in cluster-wide operations which +subsequently fail because this instances did not participate in or did +not acknowledge the operation. + +In this case, the user must manually remove the failed instance record +from the cluster and then retry the operation. To remove the failed +instance, open a management transaction against any of the running +JanusGraph instances, inspect the list of running instances to identify +the failed one, and finally remove it. +```groovy +mgmt = graph.openManagement() +mgmt.getOpenInstances() //all open instances +==>7f0001016161-dunwich1(current) +==>7f0001016161-atlantis1 +mgmt.forceCloseInstance('7f0001016161-atlantis1') //remove an instance +mgmt.commit() +``` + +The unique identifier of the current JanusGraph instance is marked with +the suffix `(current)` so that it can be easily identified. This +instance cannot be closed via the `forceCloseInstance` method and +instead should be closed via `g.close()` + +It must be ensured that the manually removed instance is indeed no +longer active. Removing an active JanusGraph instance from a cluster can +cause data inconsistencies. Hence, use this method with great care in +particular when JanusGraph is operated in an environment where instances +are automatically restarted. diff --git a/docs/static/images/relationlayout.png b/docs/advanced-topics/relationlayout.png similarity index 100% rename from docs/static/images/relationlayout.png rename to docs/advanced-topics/relationlayout.png diff --git a/docs/advanced-topics/serializer.md b/docs/advanced-topics/serializer.md new file mode 100644 index 0000000000..1ebfd67947 --- /dev/null +++ b/docs/advanced-topics/serializer.md @@ -0,0 +1,49 @@ +Datatype and Attribute Serializer Configuration +=============================================== + +JanusGraph supports a number of classes for attribute values on +properties. JanusGraph efficiently serializes primitives, primitive +arrays and `Geoshape`, `UUID`, `Date`, `ObjectNode` and `ArrayNode`. +JanusGraph supports serializing arbitrary objects as attribute values, +but these require custom serializers to be defined. + +To configure a custom attribute class with a custom serializer, follow +these steps: + +1. Implement a custom `AttributeSerializer` for the custom attribute + class + +2. Add the following configuration options where \[X\] is the custom + attribute id that must be larger than all attribute ids for already + configured custom attributes: + + 1. `attributes.custom.attribute[X].attribute-class = [Full attribute class name]` + + 2. `attributes.custom.attribute[X].serializer-class = [Full serializer class name]` + +For example, suppose we want to register a special integer attribute +class called `SpecialInt` and have implemented a custom serializer +`SpecialIntSerializer` that implements `AttributeSerializer`. We already +have 9 custom attributes configured in the configuration file, so we +would add the following lines +```conf +attributes.custom.attribute10.attribute-class = com.example.SpecialInt +attributes.custom.attribute10.serializer-class = com.example.SpecialIntSerializer +``` + +Custom Object Serialization +--------------------------- + +JanusGraph supports arbitrary objects as property attributes and can +serialize such objects to disk. For this default serializer to work for +a custom class, the following conditions must be fulfilled: + +- The class must implement AttributeSerializer + +- The class must have a no-argument constructor + +- The class must implement the `equals(Object)` method + +The last requirement is needed because JanusGraph will test both +serialization and deserialization of a custom class before persisting +data to disk. diff --git a/docs/static/images/storagelayout.png b/docs/advanced-topics/storagelayout.png similarity index 100% rename from docs/static/images/storagelayout.png rename to docs/advanced-topics/storagelayout.png diff --git a/docs/advanced.adoc b/docs/advanced.adoc deleted file mode 100644 index 039c83ec7a..0000000000 --- a/docs/advanced.adoc +++ /dev/null @@ -1,24 +0,0 @@ -[[advanced]] -= Advanced Topics - -include::advschema.adoc[] - -include::eventualconsistency.adoc[] - -include::recovery.adoc[] - -include::reindex.adoc[] - -include::bulkloading.adoc[] - -//include::multiquery.adoc[] - -include::partitioning.adoc[] - -include::serializer.adoc[] - -include::hadoop.adoc[] - -include::monitoring.adoc[] - -include::migrating.adoc[] diff --git a/docs/advblueprints.adoc b/docs/advblueprints.adoc deleted file mode 100644 index 2159b78710..0000000000 --- a/docs/advblueprints.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[advanced-blueprints]] -Advanced TinkerPop -------------------- - -http://tinkerpop.apache.org/[Tinkerpop] provides a set of common http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference#traversalstrategy[traversal strategies] that add additional functionality to graphs. - -=== Using ElementIdStrategy - -It is possible to use http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference#_elementidstrategy[ElementIdStrategy] with JanusGraph. ElementIdStrategy allow an arbitrary property to be used as the element ID instead of JanusGraph's long identifiers. - -[IMPORTANT] -The target property key must be created and covered by a unique index in JanusGraph prior to using `ElementIdStrategy` with JanusGraph, otherwise Vertex lookups will result in sequential scans of the graph. - -To prepare JanusGraph for ElementIdStrategy, first create the property key. Set the `dataType` of the property key to match the custom IDs that you intend to use. Second, build a unique composite index on the property key. The following example shows how to define and index the property key to support IdGraph with string vertex IDs. - -[source, gremlin] -graph = JanusGraphFactory.open("berkeleyje:/tmp/test") -// Define a property key and index for managed vertex IDs -mgmt = graph.openManagement() -idKey = mgmt.makePropertyKey("name").dataType(String.class).make() -mgmt.buildIndex("byName", Vertex.class).addKey(idKey).unique().buildCompositeIndex() -mgmt.commit() -// Create an that manages vertex IDs but not edge IDs -strategy = ElementIdStrategy.build().idPropertyKey("name").create() -g = GraphTraversalSource.build().with(strategy).create(graph) -// Insert example vertex with custom identifier -hercules = g.addV(id, "hercules") -g.V("hercules") diff --git a/docs/advschema.adoc b/docs/advschema.adoc deleted file mode 100644 index 6976a9ab19..0000000000 --- a/docs/advschema.adoc +++ /dev/null @@ -1,93 +0,0 @@ -[[advanced-schema]] -== Advanced Schema - -This page describes some of the advanced schema definition options that JanusGraph provides. For general information on JanusGraph's schema and how to define it, refer to <>. - - -=== Static Vertices - -Vertex labels can be defined as *static* which means that vertices with that label cannot be modified outside the transaction in which they were created. - -[source, gremlin] -mgmt = graph.openManagement() -tweet = mgmt.makeVertexLabel('tweet').setStatic().make() -mgmt.commit() - -Static vertex labels are a method of controlling the data lifecycle and useful when loading data into the graph that should not be modified after its creation. - -=== Edge and Vertex TTL - -Edge and vertex labels can be configured with a *time-to-live (TTL)*. Edges and vertices with such labels will automatically be removed from the graph when the configured TTL has passed after their initial creation. TTL configuration is useful when loading a large amount of data into the graph that is only of temporary use. Defining a TTL removes the need for manual clean up and handles the removal very efficiently. For example, it would make sense to TTL event edges such as user-page visits when those are summarized after a certain period of time or simply no longer needed for analytics or operational query processing. - -The following storage backends support edge and vertex TTL. - -* Cassandra -* HBase - -==== Edge TTL - -Edge TTL is defined on a per-edge label basis, meaning that all edges of that label have the same time-to-live. Note that the backend must support cell level TTL. Currently only Cassandra and HBase support this. - -[source, gremlin] -mgmt = graph.openManagement() -visits = mgmt.makeEdgeLabel('visits').make() -mgmt.setTTL(visits, Duration.ofDays(7)) -mgmt.commit() - -Note, that modifying an edge resets the TTL for that edge. Also note, that the TTL of an edge label can be modified but it might take some time for this change to propagate to all running JanusGraph instances which means that two different TTLs can be temporarily in use for the same label. - -==== Property TTL - -Property TTL is very similar to edge TTL and defined on a per-property key basis, meaning that all properties of that key have the same time-to-live. Note that the backend must support cell level TTL. Currently only Cassandra and HBase support this. - -[source, gremlin] -mgmt = graph.openManagement() -sensor = mgmt.makePropertyKey('sensor').cardinality(Cardinality.LIST).dataType(Double.class).make() -mgmt.setTTL(sensor, Duration.ofDays(21)) -mgmt.commit() - -As with edge TTL, modifying an existing property resets the TTL for that property and modifying the TTL for a property key might not immediately take effect. - -==== Vertex TTL - -Vertex TTL is defined on a per-vertex label basis, meaning that all vertices of that label have the same time-to-live. The configured TTL applies to the vertex, its properties, and all incident edges to ensure that the entire vertex is removed from the graph. For this reason, a vertex label must be defined as _static_ before a TTL can be set to rule out any modifications that would invalidate the vertex TTL. Vertex TTL only applies to static vertex labels. Note that the backend must support store level TTL. Currently only Cassandra and HBase support this. - -[source, gremlin] -mgmt = graph.openManagement() -tweet = mgmt.makeVertexLabel('tweet').setStatic().make() -mgmt.setTTL(tweet, Duration.ofHours(36)) -mgmt.commit() - -Note, that the TTL of a vertex label can be modified but it might take some time for this change to propagate to all running JanusGraph instances which means that two different TTLs can be temporarily in use for the same label. - -=== Multi-Properties - -As discussed in <>, JanusGraph supports property keys with SET and LIST cardinality. Hence, JanusGraph supports multiple properties with the same key on a single vertex. Furthermore, JanusGraph treats properties similarly to edges in that single-valued property annotations are allowed on properties as shown in the following example. - -[source, gremlin] -mgmt = graph.openManagement() -mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.LIST).make() -mgmt.commit() -v = graph.addVertex() -p1 = v.property('name', 'Dan LaRocque') -p1.property('source', 'web') -p2 = v.property('name', 'dalaro') -p2.property('source', 'github') -graph.tx().commit() -v.properties('name') -==> Iterable over all name properties - -These features are useful in a number of applications such as those where attaching provenance information (e.g. who added a property, when and from where?) to properties is necessary. Support for higher cardinality properties and property annotations on properties is also useful in high-concurrency, scale-out design patterns as described in <>._ - -Vertex-centric indexes and global graph indexes are supported for properties in the same manner as they are supported for edges. Refer to <> for information on defining these indexes for edges and use the corresponding API methods to define the same indexes for properties. - -=== Unidirected Edges - -Unidirected edges are edges that can only be traversed in the out-going direction. Unidirected edges have a lower storage footprint but are limited in the types of traversals they support. Unidirected edges are conceptually similar to hyperlinks in the world-wide-web in the sense that the out-vertex can traverse through the edge, but the in-vertex is unaware of its existence. - -[source, gremlin] -mgmt = graph.openManagement() -mgmt.makeEdgeLabel('author').unidirected().make() -mgmt.commit() - -Note, that unidirected edges do not get automatically deleted when their in-vertices are deleted. The user must ensure that such inconsistencies do not arise or resolve them at query time by explicitly checking vertex existence in a transaction. See the discussion in <> for more information. diff --git a/docs/appendices.adoc b/docs/appendices.adoc deleted file mode 100644 index c092b84573..0000000000 --- a/docs/appendices.adoc +++ /dev/null @@ -1,20 +0,0 @@ -[[appendices]] -= Appendices - -[[javadoc]] -[appendix] -== API Documentation (JavaDoc) - -Refer to the JanusGraph https://javadoc.io/doc/org.janusgraph/janusgraph-core/$MAVEN{project.version}[API documentation] for a complete documentation of all core APIs exposed by JanusGraph. -We *strongly* encourage all users of JanusGraph to use the Gremlin query language for any queries executed on JanusGraph and to not use JanusGraph's APIs outside of the management system. - - -//--------------------------- - -include::versions.adoc[] - -include::changelog.adoc[] - -include::upgrade.adoc[] - -include::doc-versions.adoc[] diff --git a/docs/appendices.md b/docs/appendices.md new file mode 100644 index 0000000000..b247839be5 --- /dev/null +++ b/docs/appendices.md @@ -0,0 +1,8 @@ +# Appendices +## API Documentation (JavaDoc) + +Refer to the JanusGraph [API documentation](https://javadoc.io/doc/org.janusgraph/janusgraph-core/{{ latest_version }}) +for a complete documentation of all core APIs exposed by JanusGraph. We +**strongly** encourage all users of JanusGraph to use the Gremlin query +language for any queries executed on JanusGraph and to not use +JanusGraph’s APIs outside of the management system. \ No newline at end of file diff --git a/docs/static/images/architecture-layer-diagram.svg b/docs/architecture-layer-diagram.svg similarity index 100% rename from docs/static/images/architecture-layer-diagram.svg rename to docs/architecture-layer-diagram.svg diff --git a/docs/basics.adoc b/docs/basics.adoc deleted file mode 100644 index 597ba33be0..0000000000 --- a/docs/basics.adoc +++ /dev/null @@ -1,1613 +0,0 @@ -[[basics]] -= JanusGraph Basics - -[[configuration]] -== Configuration - -A JanusGraph graph database cluster consists of one or multiple JanusGraph instances. To open a JanusGraph instance, a configuration has to be provided which specifies how JanusGraph should be set up. - -A JanusGraph configuration specifies which components JanusGraph should use, controls all operational aspects of a JanusGraph deployment, and provides a number of tuning options to get maximum performance from a JanusGraph cluster. - -At a minimum, a JanusGraph configuration must define the persistence engine that JanusGraph should use as a storage backend. <> lists all supported persistence engines and how to configure them respectively. -If advanced graph query support (e.g full-text search, geo search, or range queries) is required an additional indexing backend must be configured. See <> for details. If query performance is a concern, then caching should be enabled. Cache configuration and tuning is described in <>. - -=== Example Configurations - -Below are some example configuration files to demonstrate how to configure the most commonly used storage backends, indexing systems, and performance components. This covers only a tiny portion of the available configuration options. Refer to <> for the complete list of all options. - -==== Cassandra+Elasticsearch - -Sets up JanusGraph to use the Cassandra persistence engine running locally and a remote Elastic search indexing system: - -[source, properties] ----- -storage.backend=cql -storage.hostname=localhost - -index.search.backend=elasticsearch -index.search.hostname=100.100.101.1, 100.100.101.2 -index.search.elasticsearch.client-only=true ----- - -==== HBase+Caching - -Sets up JanusGraph to use the HBase persistence engine running remotely and uses JanusGraph's caching component for better performance. - -[source, properties] ----- -storage.backend=hbase -storage.hostname=100.100.101.1 -storage.port=2181 - -cache.db-cache = true -cache.db-cache-clean-wait = 20 -cache.db-cache-time = 180000 -cache.db-cache-size = 0.5 ----- - - -==== BerkeleyDB - -Sets up JanusGraph to use BerkeleyDB as an embedded persistence engine with Elasticsearch as an embedded indexing system. - -[source, properties] ----- -storage.backend=berkeleyje -storage.directory=/tmp/graph - -index.search.backend=elasticsearch -index.search.directory=/tmp/searchindex -index.search.elasticsearch.client-only=false -index.search.elasticsearch.local-mode=true ----- - -<> describes all of these configuration options in detail. The +conf+ directory of the JanusGraph distribution contains additional configuration examples. - -==== Further Examples - -There are several example configuration files in the `conf/` directory that can be used to get started with JanusGraph quickly. Paths to these files can be passed to `JanusGraphFactory.open(...)` as shown below: - -[source, java] ----- -// Connect to Cassandra on localhost using a default configuration -graph = JanusGraphFactory.open("conf/janusgraph-cql.properties") -// Connect to HBase on localhost using a default configuration -graph = JanusGraphFactory.open("conf/janusgraph-hbase.properties") ----- - - -=== Using Configuration - -How the configuration is provided to JanusGraph depends on the instantiation mode. - -==== JanusGraphFactory - -===== Gremlin Console - -The JanusGraph distribution contains a command line Gremlin Console which makes it easy to get started and interact with JanusGraph. Invoke `bin/gremlin.sh` (Unix/Linux) or `bin/gremlin.bat` -(Windows) to start the Console and then open a JanusGraph graph using the factory with the configuration stored in an accessible properties configuration file: - -[source, gremlin] ----- -graph = JanusGraphFactory.open('path/to/configuration.properties') ----- - -===== JanusGraph Embedded - -JanusGraphFactory can also be used to open an embedded JanusGraph graph instance from within a JVM-based user application. In that case, JanusGraph is part of the user application and the application can call upon JanusGraph directly through its public API. - -===== Short Codes - -If the JanusGraph graph cluster has been previously configured and/or only the storage backend needs to be defined, JanusGraphFactory accepts a colon-separated string representation of the storage backend name and hostname or directory. - -[source, gremlin] ----- -graph = JanusGraphFactory.open('cql:localhost') ----- - -[source, gremlin] ----- -graph = JanusGraphFactory.open('berkeleyje:/tmp/graph') ----- - - -==== JanusGraph Server - -JanusGraph, by itself, is simply a set of jar files with no thread of execution. There are two basic patterns for connecting to, and using a JanusGraph database: - -.Patterns -. JanusGraph can be used by embedding JanusGraph calls in a client program where the program provides the thread of execution. -. JanusGraph packages a long running server process that, when started, allows a remote client or logic running in a separate program to make JanusGraph calls. This long running server process is called *JanusGraph Server*. - -For the JanusGraph Server, JanusGraph uses http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#gremlin-server[Gremlin Server] of the http://tinkerpop.apache.org/[Apache TinkerPop] stack to service client requests. JanusGraph provides an out-of-the-box configuration for a quick start with JanusGraph Server, but the configuration can be changed to provide a wide range of server capabilities. - -Configuring JanusGraph Server is accomplished through a JanusGraph Server yaml configuration file located in the ./conf/gremlin-server directory in the JanusGraph distribution. To configure JanusGraph Server with a graph instance (`JanusGraph`), the JanusGraph Server configuration file requires the following settings: - -[source, yaml] ----- -... -graphs: { - graph: conf/janusgraph-berkeleyje.properties -} -plugins: - - janusgraph.imports -... ----- - -The entry for `graphs` defines the bindings to specific `JanusGraph` configurations. In the above case it binds `graph` to a JanusGraph configuration at `conf/janusgraph-berkeleyje.properties`. The `plugins` entry enables the JanusGraph Gremlin Plugin, which enables auto-imports of JanusGraph classes so that they can be referenced in remotely submitted scripts. - -Learn more about configuring and using JanusGraph Server in <>. - -===== Server Distribution - -The JanusGraph zip file contains a quick start server component that helps make it easier to get started with Gremlin Server and JanusGraph. Invoke `bin/janusgraph.sh start` to start Gremlin Server with Cassandra and Elasticsearch. - -[NOTE] -For security reasons Elasticsearch and therefore `janusgraph.sh` must be run under a non-root account - -[[configuration-global]] -=== Global Configuration - -JanusGraph distinguishes between local and global configuration options. Local configuration options apply to an individual JanusGraph instance. Global configuration options apply to all instances in a cluster. More specifically, JanusGraph distinguishes the following five scopes for configuration options: - -* *LOCAL*: These options only apply to an individual JanusGraph instance and are specified in the configuration provided when initializing the JanusGraph instance. -* *MASKABLE*: These configuration options can be overwritten for an individual JanusGraph instance by the local configuration file. If the local configuration file does not specify the option, its value is read from the global JanusGraph cluster configuration. -* *GLOBAL*: These options are always read from the cluster configuration and cannot be overwritten on an instance basis. -* *GLOBAL_OFFLINE*: Like _GLOBAL_, but changing these options requires a cluster restart to ensure that the value is the same across the entire cluster. -* *FIXED*: Like _GLOBAL_, but the value cannot be changed once the JanusGraph cluster is initialized. - -When the first JanusGraph instance in a cluster is started, the global configuration options are initialized from the provided local configuration file. Subsequently changing global configuration options is done through JanusGraph's management API. To access the management API, call `g.getManagementSystem()` on an open JanusGraph instance handle `g`. For example, to change the default caching behavior on a JanusGraph cluster: - -[source, gremlin] ----- -mgmt = graph.openManagement() -mgmt.get('cache.db-cache') -// Prints the current config setting -mgmt.set('cache.db-cache', true) -// Changes option -mgmt.get('cache.db-cache') -// Prints 'true' -mgmt.commit() -// Changes take effect ----- - -==== Changing Offline Options - -Changing configuration options does not affect running instances and only applies to newly started ones. Changing _GLOBAL_OFFLINE_ configuration options requires restarting the cluster so that the changes take effect immediately for all instances. -To change _GLOBAL_OFFLINE_ options follow these steps: - -* Close all but one JanusGraph instance in the cluster -* Connect to the single instance -* Ensure all running transactions are closed -* Ensure no new transactions are started (i.e. the cluster must be offline) -* Open the management API -* Change the configuration option(s) -* Call commit which will automatically shut down the graph instance -* Restart all instances - -Refer to the full list of configuration options in <> for more information including the configuration scope of each option. - - -[[schema]] -== Schema and Data Modeling - -Each JanusGraph graph has a schema comprised of the edge labels, property keys, and vertex labels used therein. A JanusGraph schema can either be explicitly or implicitly defined. Users are encouraged to explicitly define the graph schema during application development. An explicitly defined schema is an important component of a robust graph application and greatly improves collaborative software development. Note, that a JanusGraph schema can be evolved over time without any interruption of normal database operations. Extending the schema does not slow down query answering and does not require database downtime. - -The schema type - i.e. edge label, property key, or vertex label - is assigned to elements in the graph - i.e. edge, properties or vertices respectively - when they are first created. The assigned schema type cannot be changed for a particular element. This ensures a stable type system that is easy to reason about. - -Beyond the schema definition options explained in this section, schema types provide performance tuning options that are discussed in <>. - -=== Displaying Schema Information -There are methods to view specific elements of the graph schema within the management API. -These methods are `mgmt.printIndexes()`, `mgmt.printPropertyKeys()`, `mgmt.printVertexLabels()`, and `mgmt.printEdgeLabels()`. -There is also a method that displays all the combined output named `printSchema()`. - -[source, gremlin] -mgmt = graph.openManagement() -mgmt.printSchema() - -=== Defining Edge Labels - -Each edge connecting two vertices has a label which defines the semantics of the relationship. For instance, an edge labeled `friend` between vertices A and B encodes a friendship between the two individuals. - -To define an edge label, call `makeEdgeLabel(String)` on an open graph or management transaction and provide the name of the edge label as the argument. Edge label names must be unique in the graph. This method returns a builder for edge labels that allows to define its multiplicity. The *multiplicity* of an edge label defines a multiplicity constraint on all edges of this label, that is, a maximum number of edges between pairs of vertices. JanusGraph recognizes the following multiplicity settings. - -==== Edge Label Multiplicity - -.Multiplicity Settings -* *MULTI*: Allows multiple edges of the same label between any pair of vertices. In other words, the graph is a _multi graph_ with respect to such edge label. There is no constraint on edge multiplicity. -* *SIMPLE*: Allows at most one edge of such label between any pair of vertices. In other words, the graph is a _simple graph_ with respect to the label. Ensures that edges are unique for a given label and pairs of vertices. -* *MANY2ONE*: Allows at most one outgoing edge of such label on any vertex in the graph but places no constraint on incoming edges. The edge label `mother` is an example with MANY2ONE multiplicity since each person has at most one mother but mothers can have multiple children. -* *ONE2MANY*: Allows at most one incoming edge of such label on any vertex in the graph but places no constraint on outgoing edges. The edge label `winnerOf` is an example with ONE2MANY multiplicity since each contest is won by at most one person but a person can win multiple contests. -* *ONE2ONE*: Allows at most one incoming and one outgoing edge of such label on any vertex in the graph. The edge label 'marriedTo' is an example with ONE2ONE multiplicity since a person is married to exactly one other person. - -The default multiplicity is MULTI. The definition of an edge label is completed by calling the `make()` method on the builder which returns the defined edge label as shown in the following example. - -[source, gremlin] -mgmt = graph.openManagement() -follow = mgmt.makeEdgeLabel('follow').multiplicity(MULTI).make() -mother = mgmt.makeEdgeLabel('mother').multiplicity(MANY2ONE).make() -mgmt.commit() - -=== Defining Property Keys - -Properties on vertices and edges are key-value pairs. For instance, the property `name='Daniel'` has the key `name` and the value `'Daniel'`. Property keys are part of the JanusGraph schema and can constrain the allowed data types and cardinality of values. - -To define a property key, call `makePropertyKey(String)` on an open graph or management transaction and provide the name of the property key as the argument. Property key names must be unique in the graph, and it is recommended to avoid spaces or special characters in property names. This method returns a builder for the property keys. - -==== Property Key Data Type - -Use `dataType(Class)` to define the data type of a property key. JanusGraph will enforce that all values associated with the key have the configured data type and thereby ensures that data added to the graph is valid. For instance, one can define that the `name` key has a String data type. - -Define the data type as `Object.class` in order to allow any (serializable) value to be associated with a key. However, it is encouraged to use concrete data types whenever possible. -Configured data types must be concrete classes and not interfaces or abstract classes. JanusGraph enforces class equality, so adding a sub-class of a configured data type is not allowed. - -JanusGraph natively supports the following data types. - -.Native JanusGraph Data Types -[options="header"] -|===== -|Name |Description -|String |Character sequence -|Character |Individual character -|Boolean |true or false -|Byte |byte value -|Short |short value -|Integer |integer value -|Long |long value -|Float |4 byte floating point number -|Double |8 byte floating point number -|Date |Specific instant in time (`java.util.Date`) -|Geoshape |Geographic shape like point, circle or box -|UUID |Universally unique identifier (`java.util.UUID`) -|===== - - -[[property-cardinality]] -==== Property Key Cardinality - -Use `cardinality(Cardinality)` to define the allowed cardinality of the values associated with the key on any given vertex. - -.Cardinality Settings -* *SINGLE*: Allows at most one value per element for such key. In other words, the key->value mapping is unique for all elements in the graph. The property key `birthDate` is an example with SINGLE cardinality since each person has exactly one birth date. -* *LIST*: Allows an arbitrary number of values per element for such key. In other words, the key is associated with a list of values allowing duplicate values. Assuming we model sensors as vertices in a graph, the property key `sensorReading` is an example with LIST cardinality to allow lots of (potentially duplicate) sensor readings to be recorded. -* *SET*: Allows multiple values but no duplicate values per element for such key. In other words, the key is associated with a set of values. The property key `name` has SET cardinality if we want to capture all names of an individual (including nick name, maiden name, etc). - -The default cardinality setting is SINGLE. -Note, that property keys used on edges and properties have cardinality SINGLE. Attaching multiple values for a single key on an edge or property is not supported. - -[source, gremlin] -mgmt = graph.openManagement() -birthDate = mgmt.makePropertyKey('birthDate').dataType(Long.class).cardinality(Cardinality.SINGLE).make() -name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make() -sensorReading = mgmt.makePropertyKey('sensorReading').dataType(Double.class).cardinality(Cardinality.LIST).make() -mgmt.commit() - -=== Relation Types - -Edge labels and property keys are jointly referred to as *relation types*. Names of relation types must be unique in the graph which means that property keys and edge labels cannot have the same name. There are methods in the JanusGraph API to query for the existence or retrieve relation types which encompasses both property keys and edge labels. - -[source, gremlin] -mgmt = graph.openManagement() -if (mgmt.containsRelationType('name')) - name = mgmt.getPropertyKey('name') -mgmt.getRelationTypes(EdgeLabel.class) -mgmt.commit() - -=== Defining Vertex Labels - -Like edges, vertices have labels. Unlike edge labels, vertex labels are optional. Vertex labels are useful to distinguish different types of vertices, e.g. _user_ vertices and _product_ vertices. - -Although labels are optional at the conceptual and data model level, JanusGraph assigns all vertices a label as an internal implementation detail. Vertices created by the `addVertex` methods use JanusGraph's default label. - -To create a label, call `makeVertexLabel(String).make()` on an open graph or management transaction and provide the name of the vertex label as the argument. Vertex label names must be unique in the graph. - -[source, gremlin] -mgmt = graph.openManagement() -person = mgmt.makeVertexLabel('person').make() -mgmt.commit() -// Create a labeled vertex -person = graph.addVertex(label, 'person') -// Create an unlabeled vertex -v = graph.addVertex() -graph.tx().commit() - -=== Automatic Schema Maker - -If an edge label, property key, or vertex label has not been defined explicitly, it will be defined implicitly when it is first used during the addition of an edge, vertex or the setting of a property. The `DefaultSchemaMaker` configured for the JanusGraph graph defines such types. - -By default, implicitly created edge labels have multiplicity MULTI and implicitly created property keys have cardinality SINGLE and data type `Object.class`. Users can control automatic schema element creation by implementing and registering their own `DefaultSchemaMaker`. - -It is strongly encouraged to explicitly define all schema elements and to disable automatic schema creation by setting `schema.default=none` in the JanusGraph graph configuration. - -=== Changing Schema Elements - -The definition of an edge label, property key, or vertex label cannot be changed once its committed into the graph. However, the names of schema elements can be changed via `JanusGraphManagement.changeName(JanusGraphSchemaElement, String)` as shown in the following example where the property key `place` is renamed to `location`. - -[source, gremlin] -mgmt = graph.openManagement() -place = mgmt.getPropertyKey('place') -mgmt.changeName(place, 'location') -mgmt.commit() - -Note, that schema name changes may not be immediately visible in currently running transactions and other JanusGraph graph instances in the cluster. While schema name changes are announced to all JanusGraph instances through the storage backend, it may take a while for the schema changes to take effect and it may require a instance restart in the event of certain failure conditions - like network partitions - if they coincide with the rename. Hence, the user must ensure that either of the following holds: - -* The renamed label or key is not currently in active use (i.e. written or read) and will not be in use until all JanusGraph instances are aware of the name change. -* Running transactions actively accommodate the brief intermediate period where either the old or new name is valid based on the specific JanusGraph instance and status of the name-change announcement. For instance, that could mean transactions query for both names simultaneously. - -Should the need arise to re-define an existing schema type, it is recommended to change the name of this type to a name that is not currently (and will never be) in use. After that, a new label or key can be defined with the original name, thereby effectively replacing the old one. -However, note that this would not affect vertices, edges, or properties previously written with the existing type. Redefining existing graph elements is not supported online and must be accomplished through a batch graph transformation. - - -[[gremlin]] -== Gremlin Query Language - -image:http://tinkerpop.apache.org/docs/{tinkerpop_version}/images/gremlin-logo.png[width=370,height=143] - -http://tinkerpop.apache.org/gremlin.html[Gremlin] is JanusGraph's query language used to retrieve data from and modify data in the graph. Gremlin is a path-oriented language which succinctly expresses complex graph traversals and mutation operations. Gremlin is a http://en.wikipedia.org/wiki/Functional_programming[functional language] whereby traversal operators are chained together to form path-like expressions. For example, "from Hercules, traverse to his father and then his father's father and return the grandfather's name." - -Gremlin is a component of http://tinkerpop.apache.org[Apache TinkerPop]. It is developed independently from JanusGraph and is supported by most graph databases. By building applications on top of JanusGraph through the Gremlin query language, users avoid vendor-lock in because their application can be migrated to other graph databases supporting Gremlin. - -This section is a brief overview of the Gremlin query language. For more information on Gremlin, refer to the following resources: - -* http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference[Complete Gremlin Manual]: Reference manual for all of the Gremlin steps. -* http://tinkerpop.apache.org/docs/{tinkerpop_version}/tutorials/the-gremlin-console/[Gremlin Console Tutorial]: Learn how to use the Gremlin Console effectively to traverse and analyze a graph interactively. -* http://tinkerpop.apache.org/docs/{tinkerpop_version}/recipes/[Gremlin Recipes]: A collection of best practices and common traversal patterns for Gremlin. -* http://tinkerpop.apache.org/index.html#language-drivers[Gremlin Language Drivers]: Connect to a Gremlin Server with different programming languages, including Go, JavaScript, .NET/C#, PHP, Python, Ruby, Scala, and TypeScript. -* http://sql2gremlin.com[Gremlin for SQL developers]: Learn Gremlin using typical patterns found when querying data with SQL. - -In addition to these resources, <> explains how Gremlin can be used in different programming languages to query a JanusGraph Server. - -=== Introductory Traversals - -A Gremlin query is a chain of operations/functions that are evaluated from left to right. A simple grandfather query is provided below over the _Graph of the Gods_ dataset discussed in <>. - -[source, gremlin] -gremlin> g.V().has('name', 'hercules').out('father').out('father').values('name') -==>saturn - -The query above can be read: - -. `g`: for the current graph traversal. -. `V`: for all vertices in the graph -. `has('name', 'hercules')`: filters the vertices down to those with name property "hercules" (there is only one). -. `out('father')`: traverse outgoing father edge's from Hercules. -. `out('father')`: traverse outgoing father edge's from Hercules' father's vertex (i.e. Jupiter). -. `name`: get the name property of the "hercules" vertex's grandfather. - -Taken together, these steps form a path-like traversal query. Each step can be decomposed and its results demonstrated. This style of building up a traversal/query is useful when constructing larger, complex query chains. - -[source, gremlin] -gremlin> g -==>graphtraversalsource[janusgraph[cql:127.0.0.1], standard] -gremlin> g.V().has('name', 'hercules') -==>v[24] -gremlin> g.V().has('name', 'hercules').out('father') -==>v[16] -gremlin> g.V().has('name', 'hercules').out('father').out('father') -==>v[20] -gremlin> g.V().has('name', 'hercules').out('father').out('father').values('name') -==>saturn - -For a sanity check, it is usually good to look at the properties of each return, not the assigned long id. - -[source, gremlin] -gremlin> g.V().has('name', 'hercules').values('name') -==>hercules -gremlin> g.V().has('name', 'hercules').out('father').values('name') -==>jupiter -gremlin> g.V().has('name', 'hercules').out('father').out('father').values('name') -==>saturn - -Note the related traversal that shows the entire father family tree branch of Hercules. This more complicated traversal is provided in order to demonstrate the flexibility and expressivity of the language. A competent grasp of Gremlin provides the JanusGraph user the ability to fluently navigate the underlying graph structure. - -[source, gremlin] -gremlin> g.V().has('name', 'hercules').repeat(out('father')).emit().values('name') -==>jupiter -==>saturn - -Some more traversal examples are provided below. - -[source, gremlin] -gremlin> hercules = g.V().has('name', 'hercules').next() -==>v[1536] -gremlin> g.V(hercules).out('father', 'mother').label() -==>god -==>human -gremlin> g.V(hercules).out('battled').label() -==>monster -==>monster -==>monster -gremlin> g.V(hercules).out('battled').valueMap() -==>{name=nemean} -==>{name=hydra} -==>{name=cerberus} - -Each _step_ (denoted by a separating `.`) is a function that operates on the objects emitted from the previous step. There are numerous steps in the Gremlin language (see http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#graph-traversal-steps[Gremlin Steps]). By simply changing a step or order of the steps, different traversal semantics are enacted. The example below returns the name of all the people that have battled the same monsters as Hercules who themselves are not Hercules (i.e. "co-battlers" or perhaps, "allies"). - -Given that _The Graph of the Gods_ only has one battler (Hercules), another battler (for the sake of example) is added to the graph with Gremlin showcasing how vertices and edges are added to the graph. - -[source, gremlin] -gremlin> theseus = graph.addVertex('human') -==>v[3328] -gremlin> theseus.property('name', 'theseus') -==>null -gremlin> cerberus = g.V().has('name', 'cerberus').next() -==>v[2816] -gremlin> battle = theseus.addEdge('battled', cerberus, 'time', 22) -==>e[7eo-2kg-iz9-268][3328-battled->2816] -gremlin> battle.values('time') -==>22 - -When adding a vertex, an optional vertex label can be provided. An edge label must be specified when adding edges. Properties as key-value pairs can be set on both vertices and edges. When a property key is defined with SET or LIST cardinality, `addProperty` must be used when adding a respective property to a vertex. - -[source, gremlin] -gremlin> g.V(hercules).as('h').out('battled').in('battled').where(neq('h')).values('name') -==>theseus - -The example above has 4 chained functions: `out`, `in`, `except`, and `values` (i.e. `name` is shorthand for `values('name')`). The function signatures of each are itemized below, where `V` is vertex and `U` is any object, where `V` is a subset of `U`. - -. `out: V -> V` -. `in: V -> V` -. `except: U -> U` -. `values: V -> U` - -When chaining together functions, the incoming type must match the outgoing type, where `U` matches anything. Thus, the "co-battled/ally" traversal above is correct. - -[NOTE] -The Gremlin overview presented in this section focused on the Gremlin-Groovy language implementation used in the Gremlin Console. Refer to <> for information about connecting to JanusGraph with other languages than Groovy and independent of the Gremlin Console. - -=== Iterating the Traversal - -One convenient feature of the Gremlin Console is that it automatically iterates all results from a query executed from the `gremlin>` prompt. This works well within the http://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop[REPL] environment as it shows you the results as a String. As you transition towards writing a Gremlin application, it is important to understand how to iterate a traversal explicitly because your application's traversals will not iterate automatically. These are some of the common ways to iterate the http://tinkerpop.apache.org/javadocs/{tinkerpop_version}/full/org/apache/tinkerpop/gremlin/process/traversal/Traversal.html[`Traversal`]: - -* `iterate()` - Zero results are expected or can be ignored. -* `next()` - Get one result. Make sure to check `hasNext()` first. -* `next(int n)` - Get the next `n` results. Make sure to check `hasNext()` first. -* `toList()` - Get all results as a list. If there are no results, an empty list is returned. - -A Java code example is shown below to demonstrate these concepts: - -[source, java] -Traversal t = g.V().has("name", "pluto"); // Define a traversal -// Note the traversal is not executed/iterated yet -Vertex pluto = null; -if (t.hasNext()) { // Check if results are available - pluto = g.V().has("name", "pluto").next(); // Get one result - g.V(pluto).drop().iterate(); // Execute a traversal to drop pluto from graph -} -// Note the traversal can be cloned for reuse -Traversal tt = t.asAdmin().clone(); -if (tt.hasNext()) { - System.err.println("pluto was not dropped!"); -} -List gods = g.V().hasLabel("god").toList(); // Find all the gods - -[[server]] -== JanusGraph Server - -JanusGraph uses the http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#gremlin-server[Gremlin Server] engine as the server component to process and answer client queries. When packaged in JanusGraph, Gremlin Server is called JanusGraph Server. - -JanusGraph Server must be started manually in order to use it. JanusGraph Server provides a way to remotely execute Gremlin traversals against one or more JanusGraph instances hosted within it. This section will describe how to use the WebSocket configuration, as well as describe how to configure JanusGraph Server to handle HTTP endpoint interactions. For information about how to connect to a JanusGraph Server from different languages refer to <>. - -[[server-getting-started]] -=== Getting Started - -==== Using the Pre-Packaged Distribution - -The JanusGraph https://github.com/JanusGraph/janusgraph/releases[release] comes pre-configured to run JanusGraph Server out of the box leveraging a sample Cassandra and Elasticsearch configuration to allow users to get started quickly with JanusGraph Server. This configuration defaults to client applications that can connect to JanusGraph Server via WebSocket with a custom subprotocol. There are a number of clients developed in different languages to help support the subprotocol. The most familiar client to use the WebSocket interface is the Gremlin Console. The quick-start bundle is not intended to be representative of a production installation, but does provide a way to perform development with JanusGraph Server, run tests and see how the components are wired together. To use this default configuration: - -* Download a copy of the current `janusgraph-$VERSION.zip` file from the https://github.com/JanusGraph/janusgraph/releases[Releases page] -* Unzip it and enter the `janusgraph-$VERSION` directory -* Run `bin/janusgraph.sh start`. This step will start Gremlin Server with Cassandra/ES forked into a separate process. Note for security reasons Elasticsearch and therefore `janusgraph.sh` must be run under a non-root account. - -[source,bourne] ----- -$ bin/janusgraph.sh start -Forking Cassandra... -Running `nodetool statusthrift`.. OK (returned exit status 0 and printed string "running"). -Forking Elasticsearch... -Connecting to Elasticsearch (127.0.0.1:9300)... OK (connected to 127.0.0.1:9300). -Forking Gremlin-Server... -Connecting to Gremlin-Server (127.0.0.1:8182)... OK (connected to 127.0.0.1:8182). -Run gremlin.sh to connect. ----- - -[[first-example-connecting-gremlin-server]] -===== Connecting to Gremlin Server - -After running janusgraph.sh, Gremlin Server will be ready to listen for WebSocket connections. The easiest way to test the connection is with Gremlin Console. - -Start http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#gremlin-console[Gremlin Console] with `bin/gremlin.sh` and use the `:remote` and `:>` commands to issue Gremlin to Gremlin Server: - -[source, text] ----- -$ bin/gremlin.sh - \,,,/ - (o o) ------oOOo-(3)-oOOo----- -plugin activated: tinkerpop.server -plugin activated: tinkerpop.hadoop -plugin activated: tinkerpop.utilities -plugin activated: janusgraph.imports -plugin activated: tinkerpop.tinkergraph -gremlin> :remote connect tinkerpop.server conf/remote.yaml -==>Connected - localhost/127.0.0.1:8182 -gremlin> :> graph.addVertex("name", "stephen") -==>v[256] -gremlin> :> g.V().values('name') -==>stephen ----- - -The `:remote` command tells the console to configure a remote connection to Gremlin Server using the `conf/remote.yaml` file to connect. That file points to a Gremlin Server instance running on `localhost`. The `:>` is the "submit" command which sends the Gremlin on that line to the currently active remote. By default remote conenctions are sessionless, meaning that each line sent in the console is interpreted as a single request. Multiple statements can be sent on a single line using a semicolon as the delimiter. Alternately, you can establish a console with a session by specifying https://tinkerpop.apache.org/docs/current/reference/#sessions[session] when creating the connection. A https://tinkerpop.apache.org/docs/current/reference/#console-sessions[console session] allows you to reuse variables across several lines of input. - ----- -gremlin> :remote connect tinkerpop.server conf/remote.yaml -==>Configured localhost/127.0.0.1:8182 -gremlin> graph -==>standardjanusgraph[cql:[127.0.0.1]] -gremlin> g -==>graphtraversalsource[standardjanusgraph[cql:[127.0.0.1]], standard] -gremlin> g.V() -gremlin> user = "Chris" -==>Chris -gremlin> graph.addVertex("name", user) -No such property: user for class: Script21 -Type ':help' or ':h' for help. -Display stack trace? [yN] -gremlin> :remote connect tinkerpop.server conf/remote.yaml session -==>Configured localhost/127.0.0.1:8182-[9acf239e-a3ed-4301-b33f-55c911e04052] -gremlin> g.V() -gremlin> user = "Chris" -==>Chris -gremlin> user -==>Chris -gremlin> graph.addVertex("name", user) -==>v[4344] -gremlin> g.V().values('name') -==>Chris ----- - -=== Cleaning up after the Pre-Packaged Distribution - -If you want to start fresh and remove the database and logs you can use the clean command with `janusgraph.sh`. The server should be stopped before running the clean operation. -[source, text] ----- -$ cd /Path/to/janusgraph/janusgraph-0.2.0-hadoop2/ -$ ./bin/janusgraph.sh stop -Killing Gremlin-Server (pid 91505)... -Killing Elasticsearch (pid 91402)... -Killing Cassandra (pid 91219)... -$ ./bin/janusgraph.sh clean -Are you sure you want to delete all stored data and logs? [y/N] y -Deleted data in /Path/to/janusgraph/janusgraph-0.2.0-hadoop2/db -Deleted logs in /Path/to/janusgraph/janusgraph-0.2.0-hadoop2/log ----- - -=== JanusGraph Server as a WebSocket Endpoint - -The default configuration described in <> is already a WebSocket configuration. If you want to alter the default configuration to work with your own Cassandra or HBase environment rather than use the quick start environment, follow these steps: - -.To Configure JanusGraph Server For WebSocket -. Test a local connection to a JanusGraph database first. This step applies whether using the Gremlin Console to test the connection, or whether connecting from a program. Make appropriate changes in a properties file in the ./conf directory for your environment. For example, edit ./conf/janusgraph-hbase.properties and make sure the storage.backend, storage.hostname and storage.hbase.table parameters are specified correctly. For more information on configuring JanusGraph for various storage backends, see <>. Make sure the properties file contains the following line: -+ -``` -gremlin.graph=org.janusgraph.core.JanusGraphFactory -``` - -. Once a local configuration is tested and you have a working properties file, copy the properties file from the ./conf directory to the ./conf/gremlin-server directory. -+ -``` -cp conf/janusgraph-hbase.properties conf/gremlin-server/socket-janusgraph-hbase-server.properties -``` -+ -. Copy ./conf/gremlin-server/gremlin-server.yaml to a new file called socket-gremlin-server.yaml. Do this in case you need to refer to the original version of the file -+ -``` -cp conf/gremlin-server/gremlin-server.yaml conf/gremlin-server/socket-gremlin-server.yaml -``` -+ -. Edit the socket-gremlin-server.yaml file and make the following updates: -.. If you are planning to connect to JanusGraph Server from something other than localhost, update the IP address for host: -+ -``` -host: 10.10.10.100 -``` -+ -.. Update the graphs section to point to your new properties file so the JanusGraph Server can find and connect to your JanusGraph instance: -+ -``` -graphs: { - graph: conf/gremlin-server/socket-janusgraph-hbase-server.properties} -``` -+ -. Start the JanusGraph Server, specifying the yaml file you just configured: -+ -``` -bin/gremlin-server.sh ./conf/gremlin-server/socket-gremlin-server.yaml -``` -IMPORTANT: Do not use bin/janusgraph.sh. That starts the default configuration, which starts a separate Cassandra/Elasticsearch environment. -+ -. The JanusGraph Server should now be running in WebSocket mode and can be tested by following the instructions in <> - - - -=== JanusGraph Server as a HTTP Endpoint - -The default configuration described in <> is a WebSocket configuration. If you want to alter the default configuration in order to use JanusGraph Server as an HTTP endpoint for your JanusGraph database, follow these steps: - -.To Configure JanusGraph Server for HTTP -. Test a local connection to a JanusGraph database first. This step applies whether using the Gremlin Console to test the connection, or whether connecting from a program. Make appropriate changes in a properties file in the ./conf directory for your environment. For example, edit ./conf/janusgraph-hbase.properties and make sure the storage.backend, storage.hostname and storage.hbase.table parameters are specified correctly. For more information on configuring JanusGraph for various storage backends, see <>. Make sure the properties file contains the following line: -+ -``` -gremlin.graph=org.janusgraph.core.JanusGraphFactory -``` - -. Once a local configuration is tested and you have a working properties file, copy the properties file from the ./conf directory to the ./conf/gremlin-server directory. -+ -``` -cp conf/janusgraph-hbase.properties conf/gremlin-server/http-janusgraph-hbase-server.properties -``` -+ -. Copy ./conf/gremlin-server/gremlin-server.yaml to a new file called http-gremlin-server.yaml. Do this in case you need to refer to the original version of the file -+ -``` -cp conf/gremlin-server/gremlin-server.yaml conf/gremlin-server/http-gremlin-server.yaml -``` -+ -. Edit the http-gremlin-server.yaml file and make the following updates: -.. If you are planning to connect to JanusGraph Server from something other than localhost, update the IP address for host: -+ -``` -host: 10.10.10.100 -``` -+ -.. Update the channelizer setting to specify the HttpChannelizer: -+ -``` -channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer -``` -+ -.. Update the graphs section to point to your new properties file so the JanusGraph Server can find and connect to your JanusGraph instance: -+ -``` -graphs: { - graph: conf/gremlin-server/http-janusgraph-hbase-server.properties} -``` -+ -. Start the JanusGraph Server, specifying the yaml file you just configured: -+ -``` -bin/gremlin-server.sh ./conf/gremlin-server/http-gremlin-server.yaml -``` -+ -. The JanusGraph Server should now be running in HTTP mode and available for testing. *curl* can be used to verify the server is working: -+ -``` -curl -XPOST -Hcontent-type:application/json -d '{"gremlin":"g.V().count()"}' http://[IP for JanusGraph server host]:8182 -``` - -=== Advanced JanusGraph Server Configurations - -==== WebSocket versus HTTP - -JanusGraph Server must be run with a configuration for either WebSocket (for Gremlin Console or other WebSocket programs) or re-configured for HTTP to work with HTTP clients. - -Currently, there is no way to configure a single running instance of JanusGraph Server to communicate simultaneously with clients talking WebSocket and HTTP. -However, it is possible to create configuration files (following the steps in this chapter) for both WebSocket and HTTP and start two instances of the JanusGraph Server by pointing it to the various configurations when it is started. -In this way, the same JanusGraph database instance can be reached through different JanusGraph Servers through both WebSocket and HTTP calls. -Make sure the port parameter in each yaml configuration is different if starting more than one JanusGraph Server on the same machine: - -``` -port: 8182 -``` - -In the future, the option to have one running instance of JanusGraph Server supporting multiple protocols may be possible. - -[[gremlin-server-with-janusgraph]] -==== Using TinkerPop Gremlin Server with JanusGraph - -Since JanusGraph Server is a TinkerPop Gremlin Server packaged with configuration files for JanusGraph, a version compatible -TinkerPop Gremlin Server can be downloaded separately and used with JanusGraph. Get started by link:http://tinkerpop.apache.org/downloads.html[downloading] the appropriate version of Gremlin Server, which needs to <> supported by the JanusGraph version in use ({tinkerpop_version}). - -IMPORTANT: Any references to file paths in this section refer to paths under a TinkerPop distribution for Gremlin Server and not a JanusGraph distribution with the JanusGraph Server, unless specifically noted. - -Configuring a standalone Gremlin Server to work with JanusGraph is similar to configuring the packaged JanusGraph Server. You should be familiar with link:http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#_configuring_2[graph configuration]. Basically, the Gremlin Server yaml file points to graph-specific configuration files that are used to instantiate `JanusGraph` instances that it will then host. In order to instantiate these `Graph` instances, Gremlin Server requires that the appropriate libraries and dependencies for the `JanusGraph` be available on its classpath. - -For purposes of demonstration, these instructions will outline how to configure the BerkeleyDB backend for JanusGraph in Gremlin Server. As stated earlier, Gremlin Server needs JanusGraph dependencies on its classpath. Invoke the following command replacing `$VERSION` with the version of JanusGraph to use: - -[source,bourne] ----- -bin/gremlin-server.sh -i org.janusgraph janusgraph-all $VERSION ----- - -When this process completes, Gremlin Server should now have all the JanusGraph dependencies available to it and will thus be able to instantiate `JanusGraph` objects. - -IMPORTANT: The above command uses Groovy Grape and if it is not configured properly download errors may ensue. Please refer to link:http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#gremlin-applications[this section] of the TinkerPop documentation for more information around setting up `~/.groovy/grapeConfig.xml`. - -Create a file called `GREMLIN_SERVER_HOME/conf/janusgraph.properties` with the following contents: - -[source,text] ----- -gremlin.graph=org.janusgraph.core.JanusGraphFactory -storage.backend=berkeleyje -storage.directory=db/berkeley ----- - -Configuration of other backends is similar. See <>. If using Cassandra, then use Cassandra configuration options in the `janusgraph.properties` file. The only important piece to leave unchanged is the `gremlin.graph` setting which should always use `JanusGraphFactory`. This setting tells Gremlin Server how to instantiate a `JanusGraph` instance. - -Next create a file called `GREMLIN_SERVER_HOME/conf/gremlin-server-janusgraph.yaml` that has the following contents: - -[source,yaml] ----- -host: localhost -port: 8182 -graphs: { - graph: conf/janusgraph.properties} -plugins: - - janusgraph.imports -scriptEngines: { - gremlin-groovy: { - scripts: [scripts/janusgraph.groovy]}} -serializers: - - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }} - - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { serializeResultToString: true }} -metrics: { - slf4jReporter: {enabled: true, interval: 180000}} ----- - -.There are several important parts to this configuration file as they relate to JanusGraph. -. In the `graphs` map, there is a key called `graph` and its value is `conf/janusgraph.properties`. This tells Gremlin Server to instantiate a `Graph` instance called "graph" and use the `conf/janusgraph.properties` file to configure it. The "graph" key becomes the unique name for the `Graph` instance in Gremlin Server and it can be referenced as such in the scripts submitted to it. -. In the `plugins` list, there is a reference to `janusgraph.imports`, which tells Gremlin Server to initialize the "JanusGraph Plugin". The "JanusGraph Plugin" will auto-import JanusGraph specific classes for usage in scripts. -. Note the `scripts` key and the reference to `scripts/janusgraph.groovy`. This Groovy file is an initialization script for Gremlin Server and that particular ScriptEngine. Create `scripts/janusgraph.groovy` with the following contents: - - -[source,groovy] ----- -def globals = [:] -globals << [g : graph.traversal()] ----- - -The above script creates a `Map` called `globals` and assigns to it a key/value pair. The key is `g` and its value is a `TraversalSource` generated from `graph`, which was configured for Gremlin Server in its configuration file. At this point, there are now two global variables available to scripts provided to Gremlin Server - `graph` and `g`. - -At this point, Gremlin Server is configured and can be used to connect to a new or existing JanusGraph database. To start the server: - -[source,bourne] ----- -$ bin/gremlin-server.sh conf/gremlin-server-janusgraph.yaml -[INFO] GremlinServer - - \,,,/ - (o o) ------oOOo-(3)-oOOo----- - -[INFO] GremlinServer - Configuring Gremlin Server from conf/gremlin-server-janusgraph.yaml -[INFO] MetricManager - Configured Metrics Slf4jReporter configured with interval=180000ms and loggerName=org.apache.tinkerpop.gremlin.server.Settings$Slf4jReporterMetrics -[INFO] GraphDatabaseConfiguration - Set default timestamp provider MICRO -[INFO] GraphDatabaseConfiguration - Generated unique-instance-id=7f0000016240-ubuntu1 -[INFO] Backend - Initiated backend operations thread pool of size 8 -[INFO] KCVSLog$MessagePuller - Loaded unidentified ReadMarker start time 2015-10-02T12:28:24.411Z into org.janusgraph.diskstorage.log.kcvs.KCVSLog$MessagePuller@35399441 -[INFO] GraphManager - Graph [graph] was successfully configured via [conf/janusgraph.properties]. -[INFO] ServerGremlinExecutor - Initialized Gremlin thread pool. Threads in pool named with pattern gremlin-* -[INFO] ScriptEngines - Loaded gremlin-groovy ScriptEngine -[INFO] GremlinExecutor - Initialized gremlin-groovy ScriptEngine with scripts/janusgraph.groovy -[INFO] ServerGremlinExecutor - Initialized GremlinExecutor and configured ScriptEngines. -[INFO] ServerGremlinExecutor - A GraphTraversalSource is now bound to [g] with graphtraversalsource[standardjanusgraph[berkeleyje:db/berkeley], standard] -[INFO] AbstractChannelizer - Configured application/vnd.gremlin-v1.0+gryo with org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0 -[INFO] AbstractChannelizer - Configured application/vnd.gremlin-v1.0+gryo-stringd with org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0 -[INFO] GremlinServer$1 - Gremlin Server configured with worker thread pool of 1, gremlin pool of 8 and boss thread pool of 1. -[INFO] GremlinServer$1 - Channel started at port 8182. ----- - -The following section explains how to connect to the running server. - -===== Connecting to JanusGraph via Gremlin Server - -Gremlin Server will be ready to listen for WebSocket connections when it is started. The easiest way to test the connection is with Gremlin Console. - -Follow the instructions here <> to verify the Gremlin Server is working. - -IMPORTANT: A difference you should understand is that when working with JanusGraph Server, the Gremlin Console is started from underneath the JanusGraph distribution and when following the test instructions here for a standalone Gremlin Server, the Gremlin Console is started from under the TinkerPop distribution. - - -[source,java] ----- -GryoMapper mapper = GryoMapper.build().addRegistry(JanusGraphIoRegistry.INSTANCE).create(); -Cluster cluster = Cluster.build().serializer(new GryoMessageSerializerV1d0(mapper)).create(); -Client client = cluster.connect(); -client.submit("g.V()").all().get(); ----- - -By adding the `JanusGraphIoRegistry` to the `org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0`, the driver will know how to properly deserialize custom data types returned by JanusGraph. - -=== Extending JanusGraph Server - -It is possible to extend Gremlin Server with other means of communication by implementing the interfaces that it provides and leverage this with JanusGraph. See more details in the appropriate TinkerPop documentation. - -include::deploymentscenarios.adoc[] - -include::configuredgraphfactory.adoc[] - -[[indexes]] -== Indexing for Better Performance - -JanusGraph supports two different kinds of indexing to speed up query processing: *graph indexes* and *vertex-centric indexes*. Most graph queries start the traversal from a list of vertices or edges that are identified by their properties. Graph indexes make these global retrieval operations efficient on large graphs. Vertex-centric indexes speed up the actual traversal through the graph, in particular when traversing through vertices with many incident edges. - - -[[graph-indexes]] -=== Graph Index - -Graph indexes are global index structures over the entire graph which allow efficient retrieval of vertices or edges by their properties for sufficiently selective conditions. For instance, consider the following queries - -[source, gremlin] -g.V().has('name', 'hercules') -g.E().has('reason', textContains('loves')) - -The first query asks for all vertices with the name `hercules`. The second asks for all edges where the property reason contains the word `loves`. Without a graph index answering those queries would require a full scan over all vertices or edges in the graph to find those that match the given condition which is very inefficient and infeasible for huge graphs. - -JanusGraph distinguishes between two types of graph indexes: *composite* and *mixed* indexes. Composite indexes are very fast and efficient but limited to equality lookups for a particular, previously-defined combination of property keys. Mixed indexes can be used for lookups on any combination of indexed keys and support multiple condition predicates in addition to equality depending on the backing index store. - -Both types of indexes are created through the JanusGraph management system and the index builder returned by `JanusGraphManagement.buildIndex(String, Class)` where the first argument defines the name of the index and the second argument specifies the type of element to be indexed (e.g. `Vertex.class`). The name of a graph index must be unique. -Graph indexes built against newly defined property keys, i.e. property keys that are defined in the same management transaction as the index, are immediately available. The same applies to graph indexes that are constrained to a label that is created in the same management transaction as the index. Graph indexes built against property keys that are already in use without being constrained to a newly created label require the execution of a <> to ensure that the index contains all previously added elements. Until the reindex procedure has completed, the index will not be available. It is encouraged to define graph indexes in the same transaction as the initial schema. - -[NOTE] -In the absence of an index, JanusGraph will default to a full graph scan in order to retrieve the desired list of vertices. While this produces the correct result set, the graph scan can be very inefficient and lead to poor overall system performance in a production environment. Enable the `force-index` configuration option in production deployments of JanusGraph to prohibit graph scans. - -==== Composite Index - -Composite indexes retrieve vertices or edges by one or a (fixed) composition of multiple keys. -Consider the following composite index definitions. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -name = mgmt.getPropertyKey('name') -age = mgmt.getPropertyKey('age') -mgmt.buildIndex('byNameComposite', Vertex.class).addKey(name).buildCompositeIndex() -mgmt.buildIndex('byNameAndAgeComposite', Vertex.class).addKey(name).addKey(age).buildCompositeIndex() -mgmt.commit() -//Wait for the index to become available -ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call() -ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get() -mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get() -mgmt.commit() - -First, two property keys `name` and `age` are already defined. Next, a simple composite index on just the name property key is built. JanusGraph will use this index to answer the following query. - -[source, gremlin] -g.V().has('name', 'hercules') - -The second composite graph index includes both keys. JanusGraph will use this index to answer the following query. - -[source, gremlin] -g.V().has('age', 30).has('name', 'hercules') - -Note, that all keys of a composite graph index must be found in the query's equality conditions for this index to be used. For example, the following query cannot be answered with either of the indexes because it only contains a constraint on `age` but not `name`. - -[source, gremlin] -g.V().has('age', 30) - -Also note, that composite graph indexes can only be used for equality constraints like those in the queries above. The following query would be answered with just the simple composite index defined on the `name` key because the age constraint is not an equality constraint. - -[source, gremlin] -g.V().has('name', 'hercules').has('age', inside(20, 50)) - -Composite indexes do not require configuration of an external indexing backend and are supported through the primary storage backend. Hence, composite index modifications are persisted through the same transaction as graph modifications which means that those changes are atomic and/or consistent if the underlying storage backend supports atomicity and/or consistency. - -[NOTE] -A composite index may comprise just one or multiple keys. A composite index with just one key is sometimes referred to as a key-index. - -[[index-unique]] -===== Index Uniqueness - -Composite indexes can also be used to enforce property uniqueness in the graph. If a composite graph index is defined as `unique()` there can be at most one vertex or edge for any given concatenation of property values associated with the keys of that index. -For instance, to enforce that names are unique across the entire graph the following composite graph index would be defined. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -name = mgmt.getPropertyKey('name') -mgmt.buildIndex('byNameUnique', Vertex.class).addKey(name).unique().buildCompositeIndex() -mgmt.commit() -//Wait for the index to become available -ManagementSystem.awaitGraphIndexStatus(graph, 'byNameUnique').call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("byNameUnique"), SchemaAction.REINDEX).get() -mgmt.commit() - -[NOTE] -To enforce uniqueness against an eventually consistent storage backend, the <> of the index must be explicitly set to enabling locking. - -[[index-mixed]] -==== Mixed Index - -Mixed indexes retrieve vertices or edges by any combination of previously added property keys. -Mixed indexes provide more flexibility than composite indexes and support additional condition predicates beyond equality. On the other hand, mixed indexes are slower for most equality queries than composite indexes. - -Unlike composite indexes, mixed indexes require the configuration of an <> and use that indexing backend to execute lookup operations. JanusGraph can support multiple indexing backends in a single installation. Each indexing backend must be uniquely identified by name in the JanusGraph configuration which is called the *indexing backend name*. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -name = mgmt.getPropertyKey('name') -age = mgmt.getPropertyKey('age') -mgmt.buildIndex('nameAndAge', Vertex.class).addKey(name).addKey(age).buildMixedIndex("search") -mgmt.commit() -//Wait for the index to become available -ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get() -mgmt.commit() - -The example above defines a mixed index containing the property keys `name` and `age`. The definition refers to the indexing backend name `search` so that JanusGraph knows which configured indexing backend it should use for this particular index. The `search` parameter specified in the buildMixedIndex call must match the second clause in the JanusGraph configuration definition like this: index.*search*.backend If the index was named 'solrsearch' then the configuration definition would appear like this: index.*solrsearch*.backend. - -The mgmt.buildIndex example specified above uses text search as its default behavior. An index statement that explicitly defines the index as a text index can be written as follows: - -[source,gremlin] -mgmt.buildIndex('nameAndAge',Vertex.class).addKey(name,Mapping.TEXT.getParameter()).addKey(age,Mapping.TEXT.getParameter()).buildMixedIndex("search") - -See <> for more information on text and string search options, and see the documentation section specific to the indexing backend in use for more details on how each backend handles text versus string searches. - -While the index definition example looks similar to the composite index above, it provides greater query support and can answer _any_ of the following queries. - -[source, gremlin] -g.V().has('name', textContains('hercules')).has('age', inside(20, 50)) -g.V().has('name', textContains('hercules')) -g.V().has('age', lt(50)) - -Mixed indexes support full-text search, range search, geo search and others. Refer to <> for a list of predicates supported by a particular indexing backend. - -[NOTE] -Unlike composite indexes, mixed indexes do not support uniqueness. - -===== Adding Property Keys - -Property keys can be added to an existing mixed index which allows subsequent queries to include this key in the query condition. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -location = mgmt.makePropertyKey('location').dataType(Geoshape.class).make() -nameAndAge = mgmt.getGraphIndex('nameAndAge') -mgmt.addIndexKey(nameAndAge, location) -mgmt.commit() -//Previously created property keys already have the status ENABLED, but -//our newly created property key "location" needs to REGISTER so we wait for both statuses -ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').status(SchemaStatus.REGISTERED, SchemaStatus.ENABLED).call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get() -mgmt.commit() - - -To add a newly defined key, we first retrieve the existing index from the management transaction by its name and then invoke the `addIndexKey` method to add the key to this index. - -If the added key is defined in the same management transaction, it will be immediately available for querying. If the property key has already been in use, adding the key requires the execution of a <> to ensure that the index contains all previously added elements. Until the reindex procedure has completed, the key will not be available in the mixed index. - -===== Mapping Parameters - -When adding a property key to a mixed index - either through the index builder or the `addIndexKey` method - a list of parameters can be optionally specified to adjust how the property value is mapped into the indexing backend. Refer to the <> for a complete list of parameter types supported by each indexing backend. - -==== Ordering - -The order in which the results of a graph query are returned can be defined using the `order().by()` directive. The `order().by()` method expects two parameters: - -* The name of the property key by which to order the results. The results will be ordered by the value of the vertices or edges for this property key. -* The sort order: either increasing `incr` or decreasing `decr` - -For example, the query `g.V().has('name', textContains('hercules')).order().by('age', decr).limit(10)` retrieves the ten oldest individuals with 'hercules' in their name. - -When using `order().by()` it is important to note that: - -* Composite graph indexes do not natively support ordering search 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, 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. - -==== Label Constraint - -In many cases it is desirable to only index vertices or edges with a particular label. For instance, one may want to index only gods by their name and not every single vertex that has a name property. -When defining an index it is possible to restrict the index to a particular vertex or edge label using the `indexOnly` method of the index builder. The following creates a composite index for the property key `name` that indexes only vertices labeled `god`. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -name = mgmt.getPropertyKey('name') -god = mgmt.getVertexLabel('god') -mgmt.buildIndex('byNameAndLabel', Vertex.class).addKey(name).indexOnly(god).buildCompositeIndex() -mgmt.commit() -//Wait for the index to become available -ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndLabel').call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("byNameAndLabel"), SchemaAction.REINDEX).get() -mgmt.commit() - - -Label restrictions similarly apply to mixed indexes. When a composite index with label restriction is defined as unique, the uniqueness constraint only applies to properties on vertices or edges for the specified label. - -==== Composite versus Mixed Indexes - -. Use a composite index for exact match index retrievals. Composite indexes do not require configuring or operating an external index system and are often significantly faster than mixed indexes. -.. As an exception, use a mixed index for exact matches when the number of distinct values for query constraint is relatively small or if one value is expected to be associated with many elements in the graph (i.e. in case of low selectivity). -. Use a mixed indexes for numeric range, full-text or geo-spatial indexing. Also, using a mixed index can speed up the order().by() queries. - - -[[vertex-indexes]] -=== Vertex-centric Indexes - -Vertex-centric indexes are local index structures built individually per vertex. In large graphs vertices can have thousands of incident edges. Traversing through those vertices can be very slow because a large subset of the incident edges has to be retrieved and then filtered in memory to match the conditions of the traversal. Vertex-centric indexes can speed up such traversals by using localized index structures to retrieve only those edges that need to be traversed. - -Suppose that Hercules battled hundreds of monsters in addition to the three captured in the introductory <>. Without a vertex-centric index, a query asking for those monsters battled between time point `10` and `20` would require retrieving all `battled` edges even though there are only a handful of matching edges. - -[source, gremlin] -h = g.V().has('name', 'hercules').next() -g.V(h).outE('battled').has('time', inside(10, 20)).inV() - -Building a vertex-centric index by time speeds up such traversal queries. Note, this initial index example already exists in the _Graph of the Gods_ as an index named `edges`. As a result, running the steps below will result in a uniqueness constraint error. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -time = mgmt.getPropertyKey('time') -battled = mgmt.getEdgeLabel('battled') -mgmt.buildEdgeIndex(battled, 'battlesByTime', Direction.BOTH, Order.decr, time) -mgmt.commit() -//Wait for the index to become available -ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByTime').call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("battlesByTime"), SchemaAction.REINDEX).get() -mgmt.commit() - -This example builds a vertex-centric index which indexes `battled` edges in both direction by time in decreasing order. -A vertex-centric index is built against a particular edge label which is the first argument to the index construction method `JanusGraphManagement.buildEdgeIndex()`. The index only applies to edges of this label - `battled` in the example above. The second argument is a unique name for the index. The third argument is the edge direction in which the index is built. The index will only apply to traversals along edges in this direction. In this example, the vertex-centric index is built in both direction which means that time restricted traversals along `battled` edges can be served by this index in both the `IN` and `OUT` direction. JanusGraph will maintain a vertex-centric index on both the in- and out-vertex of `battled` edges. Alternatively, one could define the index to apply to the `OUT` direction only which would speed up traversals from Hercules to the monsters but not in the reverse direction. This would only require maintaining one index and hence half the index maintenance and storage cost. -The last two arguments are the sort order of the index and a list of property keys to index by. The sort order is optional and defaults to ascending order (i.e. `Order.ASC`). The list of property keys must be non-empty and defines the keys by which to index the edges of the given label. A vertex-centric index can be defined with multiple keys. - -[source, gremlin] -graph.tx().rollback() //Never create new indexes while a transaction is active -mgmt = graph.openManagement() -time = mgmt.getPropertyKey('time') -rating = mgmt.makePropertyKey('rating').dataType(Double.class).make() -battled = mgmt.getEdgeLabel('battled') -mgmt.buildEdgeIndex(battled, 'battlesByRatingAndTime', Direction.OUT, Order.decr, rating, time) -mgmt.commit() -//Wait for the index to become available -ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByRatingAndTime', 'battled').call() -//Reindex the existing data -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getRelationIndex(battled, 'battlesByRatingAndTime'), SchemaAction.REINDEX).get() -mgmt.commit() - -This example extends the schema by a `rating` property on `battled` edges and builds a vertex-centric index which indexes `battled` edges in the out-going direction by rating and time in decreasing order. Note, that the order in which the property keys are specified is important because vertex-centric indexes are prefix indexes. This means, that `battled` edges are indexed by `rating` _first_ and `time` _second_. - -[source, gremlin] -//Add some rating data -h = g.V().has('name', 'hercules').next() -g.V(h).outE('battled').property('rating', 5.0) //Add some rating properties -g.V(h).outE('battled').has('rating', gt(3.0)).inV() -g.V(h).outE('battled').has('rating', 5.0).has('time', inside(10, 50)).inV() -g.V(h).outE('battled').has('time', inside(10, 50)).inV() - -Hence, the `battlesByRatingAndTime` index can speed up the first two but not the third query. - -Multiple vertex-centric indexes can be built for the same edge label in order to support different constraint traversals. JanusGraph's query optimizer attempts to pick the most efficient index for any given traversal. Vertex-centric indexes only support equality and range/interval constraints. - -[NOTE] -The property keys used in a vertex-centric index must have an explicitly defined data type (i.e. _not_ `Object.class`) which supports a native sort order. - -If the vertex-centric index is built against an edge label that is defined in the same management transaction, the index will be immediately available for querying. If the edge label has already been in use, building a vertex-centric index against it requires the execution of a <> to ensure that the index contains all previously added edges. Until the reindex procedure has completed, the index will not be available. - -[NOTE] -JanusGraph automatically builds vertex-centric indexes per edge label and property key. That means, even with thousands of incident `battled` edges, queries like `g.V(h).out('mother')` or `g.V(h).values('age')` are efficiently answered by the local index. - -Vertex-centric indexes cannot speed up unconstrained traversals which require traversing through all incident edges of a particular label. Those traversals will become slower as the number of incident edges increases. Often, such traversals can be rewritten as constrained traversals that can utilize a vertex-centric index to ensure acceptable performance at scale. - -==== Ordered Traversals - -The following queries specify an order in which the incident edges are to be traversed. Use the `localLimit` command to retrieve a subset of the edges (in a given order) for EACH vertex that is traversed. - -[source, gremlin] -h = g..V().has('name', 'hercules').next() -g.V(h).local(outE('battled').order().by('time', decr).limit(10)).inV().values('name') -g.V(h).local(outE('battled').has('rating', 5.0).order().by('time', decr).limit(10)).values('place') - -The first query asks for the names of the 10 most recently battled monsters by Hercules. The second query asks for the places of the 10 most recent battles of Hercules that are rated 5 stars. In both cases, the query is constrained by an order on a property key with a limit on the number of elements to be returned. - -Such queries can also be efficiently answered by vertex-centric indexes if the order key matches the key of the index and the requested order (i.e. increasing or decreasing) is the same as the one defined for the index. The `battlesByTime` index would be used to answer the first query and `battlesByRatingAndTime` applies to the second. Note, that the `battlesByRatingAndTime` index cannot be used to answer the first query because an equality constraint on `rating` must be present for the second key in the index to be effective. - -[NOTE] -Ordered vertex queries are a JanusGraph extension to Gremlin which causes the verbose syntax and requires the `_()` step to convert the JanusGraph result back into a Gremlin pipeline. - - -[[tx]] -== Transactions - -Almost all interaction with JanusGraph is associated with a transaction. JanusGraph transactions are safe for concurrent use by multiple threads. Methods on a JanusGraph instance like `graph.V(...)` and `graph.tx().commit()` perform a `ThreadLocal` lookup to retrieve or create a transaction associated with the calling thread. Callers can alternatively forego `ThreadLocal` transaction management in favor of calling `graph.tx().createThreadedTx()`, which returns a reference to a transaction object with methods to read/write graph data and commit or rollback. - -JanusGraph transactions are not necessarily ACID. They can be so configured on BerkeleyDB, but they are not generally so on Cassandra or HBase, where the underlying storage system does not provide serializable isolation or multi-row atomic writes and the cost of simulating those properties would be substantial. - -This section describes JanusGraph's transactional semantics and API. - -=== Transaction Handling - -Every graph operation in JanusGraph occurs within the context of a transaction. According to the TinkerPop's transactional specification, each thread opens its own transaction against the graph database with the first operation (i.e. retrieval or mutation) on the graph: - -[source, gremlin] ----- -graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph") -juno = graph.addVertex() //Automatically opens a new transaction -juno.property("name", "juno") -graph.tx().commit() //Commits transaction ----- - -In this example, a local JanusGraph graph database is opened. Adding the vertex "juno" is the first operation (in this thread) which automatically opens a new transaction. All subsequent operations occur in the context of that same transaction until the transaction is explicitly stopped or the graph database is closed. If transactions are still open when `close()` is called, then the behavior of the outstanding transactions is technically undefined. In practice, any non-thread-bound transactions will usually be effectively rolled back, but the thread-bound transaction belonging to the thread that invoked shutdown will first be committed. Note, that both read and write operations occur within the context of a transaction. - -=== Transactional Scope - -All graph elements (vertices, edges, and types) are associated with the transactional scope in which they were retrieved or created. Under TinkerPop's default transactional semantics, transactions are automatically created with the first operation on the graph and closed explicitly using `commit()` or `rollback()`. Once the transaction is closed, all graph elements associated with that transaction become stale and unavailable. However, JanusGraph will automatically transition vertices and types into the new transactional scope as shown in this example: - -[source, gremlin] -graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph") -juno = graph.addVertex() //Automatically opens a new transaction -graph.tx().commit() //Ends transaction -juno.property("name", "juno") //Vertex is automatically transitioned - -Edges, on the other hand, are not automatically transitioned and cannot be accessed outside their original transaction. They must be explicitly transitioned: - -[source, gremlin] -e = juno.addEdge("knows", graph.addVertex()) -graph.tx().commit() //Ends transaction -e = g.E(e).next() //Need to refresh edge -e.property("time", 99) - -=== Transaction Failures - -When committing a transaction, JanusGraph will attempt to persist all changes to the storage backend. This might not always be successful due to IO exceptions, network errors, machine crashes or resource unavailability. Hence, transactions can fail. In fact, transactions *will eventually fail* in sufficiently large systems. Therefore, we highly recommend that your code expects and accommodates such failures: - -[source, gremlin] -try { - if (g.V().has("name", name).iterator().hasNext()) - throw new IllegalArgumentException("Username already taken: " + name) - user = graph.addVertex() - user.property("name", name) - graph.tx().commit() -} catch (Exception e) { - //Recover, retry, or return error message - println(e.getMessage()) -} - -The example above demonstrates a simplified user signup implementation where `name` is the name of the user who wishes to register. First, it is checked whether a user with that name already exists. If not, a new user vertex is created and the name assigned. Finally, the transaction is committed. - -If the transaction fails, a `JanusGraphException` is thrown. There are a variety of reasons why a transaction may fail. JanusGraph differentiates between _potentially temporary_ and _permanent_ failures. - -Potentially temporary failures are those related to resource unavailability and IO hiccups (e.g. network timeouts). JanusGraph automatically tries to recover from temporary failures by retrying to persist the transactional state after some delay. The number of retry attempts and the retry delay are configurable (see <>). - -Permanent failures can be caused by complete connection loss, hardware failure or lock contention. To understand the cause of lock contention, consider the signup example above and suppose a user tries to signup with username "juno". That username may still be available at the beginning of the transaction but by the time the transaction is committed, another user might have concurrently registered with "juno" as well and that transaction holds the lock on the username therefore causing the other transaction to fail. Depending on the transaction semantics one can recover from a lock contention failure by re-running the entire transaction. - -Permanent exceptions that can fail a transaction include: - -* PermanentLockingException(*Local lock contention*): Another local thread has already been granted a conflicting lock. -* PermanentLockingException(*Expected value mismatch for X: expected=Y vs actual=Z*): The verification that the value read in this transaction is the same as the one in the datastore after applying for the lock failed. In other words, another transaction modified the value after it had been read and modified. - -[[multi-thread-tx]] -=== Multi-Threaded Transactions - -JanusGraph supports multi-threaded transactions through TinkerPop's http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference#_threaded_transactions[threaded transactions]. Hence, to speed up transaction processing and utilize multi-core architectures multiple threads can run concurrently in a single transaction. - -With TinkerPop's default transaction handling, each thread automatically opens its own transaction against the graph database. To open a thread-independent transaction, use the `createThreadedTx()` method. - -[source, gremlin] -threadedGraph = graph.tx().createThreadedTx(); -threads = new Thread[10]; -for (int i=0; i> for more detail) and since the transaction is running for a long time, lock congestion and expensive transactional failures are likely. - -[source, gremlin] -v1 = graph.addVertex() -//Do many other things -v2 = graph.addVertex() -v2.property("uniqueName", "foo") -v1.addEdge("related", v2) -//Do many other things -graph.tx().commit() // This long-running tx might fail due to contention on its uniqueName lock - -One way around this is to create the vertex in a short, nested thread-independent transaction as demonstrated by the following pseudo code:: - -[source, gremlin] -v1 = graph.addVertex() -//Do many other things -tx = graph.tx().createThreadedTx() -v2 = tx.addVertex() -v2.property("uniqueName", "foo") -tx.commit() // Any lock contention will be detected here -v1.addEdge("related", g.V(v2).next()) // Need to load v2 into outer transaction -//Do many other things -graph.tx().commit() // Can't fail due to uniqueName write lock contention involving v2 - - -=== Common Transaction Handling Problems - -Transactions are started automatically with the first operation executed against the graph. One does NOT have to start a transaction manually. The method `newTransaction` is used to start <> only. - -Transactions are automatically started under the TinkerPop semantics but *not* automatically terminated. Transactions must be terminated manually with `commit()` or `rollback()`. If a `commit()` transactions fails, it should be terminated manually with `rollback()` after catching the failure. Manual termination of transactions is necessary because only the user knows the transactional boundary. - -A transaction will attempt to maintain its state from the beginning of the transaction. This might lead to unexpected behavior in multi-threaded applications as illustrated in the following artificial example:: - -[source, gremlin] -v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction -g.V(v).bothE() ->> returns nothing, v has no edges -//thread is idle for a few seconds, another thread adds edges to v -g.V(v).bothE() ->> still returns nothing because the transactional state from the beginning is maintained - -Such unexpected behavior is likely to occur in client-server applications where the server maintains multiple threads to answer client requests. It is therefore important to terminate the transaction after a unit of work (e.g. code snippet, query, etc). So, the example above should be: - -[source, gremlin] -v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction -g.V(v).bothE() -graph.tx().commit() -//thread is idle for a few seconds, another thread adds edges to v -g.V(v).bothE() ->> returns the newly added edge -graph.tx().commit() - -When using multi-threaded transactions via `newTransaction` all vertices and edges retrieved or created in the scope of that transaction are *not* available outside the scope of that transaction. Accessing such elements after the transaction has been closed will result in an exception. As demonstrated in the example above, such elements have to be explicitly refreshed in the new transaction using `g.V(existingVertex)` or `g.E(existingEdge)`. - -[[tx-config]] -=== Transaction Configuration - -JanusGraph's `JanusGraph.buildTransaction()` method gives the user the ability to configure and start a new <> against a `JanusGraph`. Hence, it is identical to `JanusGraph.newTransaction()` with additional configuration options. - -`buildTransaction()` returns a `TransactionBuilder` which allows the following aspects of a transaction to be configured: - -* `readOnly()` - makes the transaction read-only and any attempt to modify the graph will result in an exception. -* `enableBatchLoading()` - enables batch-loading for an individual transaction. This setting results in similar efficiencies as the graph-wide setting `storage.batch-loading` due to the disabling of consistency checks and other optimizations. Unlike `storage.batch-loading` this option will not change the behavior of the storage backend. -* `setTimestamp(long)` - Sets the timestamp for this transaction as communicated to the storage backend for persistence. Depending on the storage backend, this setting may be ignored. For eventually consistent backends, this is the timestamp used to resolve write conflicts. If this setting is not explicitly specified, JanusGraph uses the current time. -* `setVertexCacheSize(long size)` - The number of vertices this transaction caches in memory. The larger this number, the more memory a transaction can potentially consume. If this number is too small, a transaction might have to re-fetch data which causes delays in particular for long running transactions. -* `checkExternalVertexExistence(boolean)` - Whether this transaction should verify the existence of vertices for user provided vertex ids. Such checks requires access to the database which takes time. The existence check should only be disabled if the user is absolutely sure that the vertex must exist - otherwise data corruption can ensue. -* `checkInternalVertexExistence(boolean)` - Whether this transaction should double-check the existence of vertices during query execution. This can be useful to avoid *phantom vertices* on eventually consistent storage backends. Disabled by default. Enabling this setting can slow down query processing. -* `consistencyChecks(boolean)` - Whether JanusGraph should enforce schema level consistency constraints (e.g. multiplicity constraints). Disabling consistency checks leads to better performance but requires that the user ensures consistency confirmation at the application level to avoid inconsistencies. USE WITH GREAT CARE! - -Once, the desired configuration options have been specified, the new transaction is started via `start()` which returns a `JanusGraphTransaction`. - -[[caching]] -== JanusGraph Cache - -=== Caching - -JanusGraph employs multiple layers of data caching to facilitate fast graph traversals. The caching layers are listed here in the order they are accessed from within a JanusGraph transaction. The closer the cache is to the transaction, the faster the cache access and the higher the memory footprint and maintenance overhead. - -[[tx-cache]] -=== Transaction-Level Caching - -Within an open transaction, JanusGraph maintains two caches: - -* Vertex Cache: Caches accessed vertices and their adjacency list (or subsets thereof) so that subsequent access is significantly faster within the same transaction. Hence, this cache speeds up iterative traversals. -* Index Cache: Caches the results for index queries so that subsequent index calls can be served from memory instead of calling the index backend and (usually) waiting for one or more network round trips. - -The size of both of those is determined by the _transaction cache size_. The -transaction cache size can be configured via `cache.tx-cache-size` or on a -per transaction basis by opening a transaction via the transaction builder -`graph.buildTransaction()` and using the `setVertexCacheSize(int)` method. - -==== Vertex Cache - -The vertex cache contains vertices and the subset of their adjacency list that has been retrieved in a particular transaction. The maximum number of vertices maintained in this cache is equal to the transaction cache size. If the transaction workload is an iterative traversal, the vertex cache will significantly speed it up. If the same vertex is not accessed again in the transaction, the transaction level cache will make no difference. - -Note, that the size of the vertex cache on heap is not only determined by the number of vertices it may hold but also by the size of their adjacency list. In other words, vertices with large adjacency lists (i.e. many incident edges) will consume more space in this cache than those with smaller lists. - -Furthermore note, that modified vertices are _pinned_ in the cache, which means they cannot be evicted since that would entail loosing their changes. Therefore, transaction which contain a lot of modifications may end up with a larger than configured vertex cache. - -==== Index Cache - -The index cache contains the results of index queries executed in the context of this transaction. Subsequent identical index calls will be served from this cache and are therefore significantly cheaper. If the same index call never occurs twice in the same transaction, the index cache makes no difference. - -Each entry in the index cache is given a weight equal to `2 + result set size` and the total weight of the cache will not exceed half of the transaction cache size. - -[[db-cache]] -=== Database Level Caching - -The database level cache retains adjacency lists (or subsets thereof) across multiple transactions and beyond the duration of a single transaction. The database level cache is shared by all transactions across a database. It is more space efficient than the transaction level caches but also slightly slower to access. In contrast to the transaction level caches, the database level caches do not expire immediately after closing a transaction. Hence, the database level cache significantly speeds up graph traversals for read heavy workloads across transactions. - -<> lists all of the configuration options that pertain to JanusGraph's database level cache. This page attempts to explain their usage. - -Most importantly, the database level cache is disabled by default in the current release version of JanusGraph. To enable it, set `cache.db-cache=true`. - -==== Cache Expiration Time - -The most important setting for performance and query behavior is the cache expiration time which is configured via `cache.db-cache-time`. The cache will hold graph elements for at most that many milliseconds. If an element expires, the data will be re-read from the storage backend on the next access. - -If there is only one JanusGraph instance accessing the storage backend or if this instance is the only one modifying the graph, the cache expiration can be set to 0 which disables cache expiration. This allows the cache to hold elements indefinitely (unless they are evicted due to space constraints or on update) which provides the best cache performance. Since no other JanusGraph instance is modifying the graph, there is no danger of holding on to stale data. - -If there are multiple JanusGraph instances accessing the storage backend, the time should be set to the maximum time that can be allowed between *another* JanusGraph instance modifying the graph and this JanusGraph instance seeing the data. -If any change should be immediately visible to all JanusGraph instances, the database level cache should be disabled in a distributed setup. However, for most applications it is acceptable that a particular JanusGraph instance sees remote modifications with some delay. The larger the maximally allowed delay, the better the cache performance. -Note, that a given JanusGraph instance will always immediately see its own modifications to the graph irrespective of the configured cache expiration time. - -==== Cache Size - -The configuration option `cache.db-cache-size` controls how much heap space JanusGraph's database level cache is allowed to consume. The larger the cache, the more effective it will be. However, large cache sizes can lead to excessive GC and poor performance. - -The cache size can be configured as a percentage (expressed as a decimal between 0 and 1) of the total heap space available to the JVM running JanusGraph or as an absolute number of bytes. - -Note, that the cache size refers to the amount of heap space that is exclusively occupied by the cache. JanusGraph's other data structures and each open transaction will occupy additional heap space. If additional software layers are running in the same JVM, those may occupy a significant amount of heap space as well (e.g. Gremlin Server, embedded Cassandra, etc). Be conservative in your heap memory estimation. Configuring a cache that is too large can lead to out-of-memory exceptions and excessive GC. - -==== Clean Up Wait Time - -When a vertex is locally modified (e.g. an edge is added) all of the vertex's related database level cache entries are marked as expired and eventually evicted. This will cause JanusGraph to refresh the vertex's data from the storage backend on the next access and re-populate the cache. - -However, when the storage backend is eventually consistent, the modifications that triggered the eviction may not yet be visible. By configuring `cache.db-cache-clean-wait`, the cache will wait for at least this many milliseconds before repopulating the cache with the entry retrieved from the storage backend. - -If JanusGraph runs locally or against a storage backend that guarantees immediate visibility of modifications, this value can be set to 0. - -=== Storage Backend Caching - -Each storage backend maintains its own data caching layer. These caches benefit from compression, data compactness, coordinated expiration and are often maintained off heap which means that large caches can be used without running into garbage collection issues. While these caches can be significantly larger than the database level cache, they are also slower to access. - -The exact type of caching and its properties depends on the particular <>. Please refer to the respective documentation for more information about the caching infrastructure and how to optimize it. - -[[log]] -== Transaction Log - -JanusGraph can automatically log transactional changes for additional processing or as a record of change. To enable logging for a particular transaction, specify the name of the target log during the start of the transaction. - -[source, gremlin] -tx = graph.buildTransaction().logIdentifier('addedPerson').start() -u = tx.addVertex(label, 'human') -u.property('name', 'proteros') -u.property('age', 36) -tx.commit() - -Upon commit, any changes made during the transaction are logged to the user logging system into a log named `addedPerson`. The *user logging system* is a configurable logging backend with a JanusGraph compatible log interface. By default, the log is written to a separate store in the primary storage backend which can be configured as described below. The log identifier specified during the start of the transaction identifies the log in which the changes are recorded thereby allowing different types of changes to be recorded in separate logs for individual processing. - -[source, gremlin] -tx = graph.buildTransaction().logIdentifier('battle').start() -h = tx.traversal().V().has('name', 'hercules').next() -m = tx.addVertex(label, 'monster') -m.property('name', 'phylatax') -h.addEdge('battled', m, 'time', 22) -tx.commit() - -JanusGraph provides a user transaction log processor framework to process the recorded transactional changes. The transaction log processor is opened via `JanusGraphFactory.openTransactionLog(JanusGraph)` against a previously opened JanusGraph graph instance. One can then add processors for a particular log which holds transactional changes. - -[source, gremlin] -import java.util.concurrent.atomic.*; -import org.janusgraph.core.log.*; -import java.util.concurrent.*; -logProcessor = JanusGraphFactory.openTransactionLog(g); -totalHumansAdded = new AtomicInteger(0); -totalGodsAdded = new AtomicInteger(0); -logProcessor.addLogProcessor("addedPerson"). - setProcessorIdentifier("addedPersonCounter"). - setStartTimeNow(). - addProcessor(new ChangeProcessor() { - @Override - public void process(JanusGraphTransaction tx, TransactionId txId, ChangeState changeState) { - for (v in changeState.getVertices(Change.ADDED)) { - if (v.label().equals("human")) totalHumansAdded.incrementAndGet(); - } - } - }). - addProcessor(new ChangeProcessor() { - @Override - public void process(JanusGraphTransaction tx, TransactionId txId, ChangeState changeState) { - for (v in changeState.getVertices(Change.ADDED)) { - if (v.label().equals("god")) totalGodsAdded.incrementAndGet(); - } - } - }). - build(); - -In this example, a *log processor* is built for the user transaction log named `addedPerson` to process the changes made in transactions which used the `addedPerson` log identifier. Two *change processors* are added to this log processor. The first processor counts the number of humans added and the second counts the number of gods added to the graph. - -When a log processor is built against a particular log, such as the `addedPerson` log in the example above, it will start reading transactional change records from the log immediately upon successful construction and initialization up to the head of the log. The start time specified in the builder marks the time point in the log where the log processor will start reading records. Optionally, one can specify an identifier for the log processor in the builder. The log processor will use the identifier to regularly persist its state of processing, i.e. it will maintain a marker on the last read log record. If the log processor is later restarted with the same identifier, it will continue reading from the last read record. This is particularly useful when the log processor is supposed to run for long periods of time and is therefore likely to fail. In such failure situations, the log processor can simply be restarted with the same identifier. -It must be ensured that log processor identifiers are unique in a JanusGraph cluster in order to avoid conflicts on the persisted read markers. - -A change processor must implement the `ChangeProcessor` interface. It's `process()` method is invoked for each change record read from the log with a `JanusGraphTransaction` handle, the id of the transaction that caused the change, and a `ChangeState` container which holds the transactional changes. The change state container can be queried to retrieve individual elements that were part of the change state. In the example, all added vertices are retrieved. Refer to the API documentation for a description of all the query methods on `ChangeState`. The provided transaction id can be used to investigate the origin of the transaction which is uniquely identified by the combination of the id of the JanusGraph instance that executed the transaction (`txId.getInstanceId()`) and the instance specific transaction id (`txId.getTransactionId()`). In addition, the time of the transaction is available through `txId.getTransactionTime()`. - -Change processors are executed individually and in multiple threads. If a change processor accesses global state it must be ensured that such state allows concurrent access. While the log processor reads log records sequentially, the changes are processed in multiple threads so it cannot be guaranteed that the log order is preserved in the change processors. - -Note, that log processors run each registered change processor at least once for each record in the log which means that a single transactional change record may be processed multiple times under certain failure conditions. -One cannot add or remove change processor from a running log processor. In other words, a log processor is immutable after it is built. To change log processing, start a new log processor and shut down an existing one. - -[source, gremlin] -logProcessor.addLogProcessor("battle"). - setProcessorIdentifier("battleTimer"). - setStartTimeNow(). - addProcessor(new ChangeProcessor() { - @Override - public void process(JanusGraphTransaction tx, TransactionId txId, ChangeState changeState) { - h = tx.V().has("name", "hercules").toList().iterator().next(); - for (edge in changeState.getEdges(h, Change.ADDED, Direction.OUT, "battled")) { - if (edge.value("time")>1000) - h.property("oldFighter", true); - } - } - }). - build(); - -The log processor above processes transactions for the `battle` log identifier with a single change processor which evaluates `battled` edges that were added to Hercules. This example demonstrates that the transaction handle passed into the change processor is a normal `JanusGraphTransaction` which query the JanusGraph graph and make changes to it. - -=== Transaction Log Use Cases - -==== Record of Change - -The user transaction log can be used to keep a record of all changes made against the graph. By using separate log identifiers, changes can be recorded in different logs to distinguish separate transaction types. - -At any time, a log processor can be built which can processes all recorded changes starting from the desired start time. This can be used for forensic analysis, to replay changes against a different graph, or to compute an aggregate. - -==== Downstream Updates - -It is often the case that a JanusGraph graph cluster is part of a larger architecture. The user transaction log and the log processor framework provide the tools needed to broadcast changes to other components of the overall system without slowing down the original transactions causing the change. This is particularly useful when transaction latencies need to be low and/or there are a number of other systems that need to be alerted to a change in the graph. - -==== Triggers - -The user transaction log provides the basic infrastructure to implement triggers that can scale to a large number of concurrent transactions and very large graphs. A trigger is registered with a particular change of data and either triggers an event in an external system or additional changes to the graph. At scale, it is not advisable to implement triggers in the original transaction but rather process triggers with a slight delay through the log processor framework. The second example shows how changes to the graph can be evaluated and trigger additional modifications. - -=== Log Configuration - -There are a number of configuration options to fine tune how the log processor reads from the log. Refer to the complete list of configuration options <> for the options under the `log` namespace. To configure the user transaction log, use the `log.user` namespace. The options listed there allow the configuration of the number of threads to be used, the number of log records read in each batch, the read interval, and whether the transaction change records should automatically expire and be removed from the log after a configurable amount of time (TTL). - -include::configref.adoc[] - -[[common-questions]] -== Common Questions - -=== Accidental type creation - -By default, JanusGraph will automatically create property keys and edge labels when a new type is encountered. It is strongly encouraged that users explicitly schemata as documented in <> before loading any data and disable automatic type creation by setting the option `schema.default = none`. - -Automatic type creation can cause problems in multi-threaded or highly concurrent environments. Since JanusGraph needs to ensure that types are unique, multiple attempts at creating the same type will lead to locking or other exceptions. It is generally recommended to create all needed types up front or in one batch when new property keys and edge labels are needed. - -=== Custom Class Datatype - -JanusGraph supports arbitrary objects as attribute values on properties. To use a custom class as data type in JanusGraph, either register a custom serializer or ensure that the class has a no-argument constructor and implements the `equals` method because JanusGraph will verify that it can successfully de-/serialize objects of that class. Please see <> for more information. - -=== Transactional Scope for Edges - -Edges should not be accessed outside the scope in which they were originally created or retrieved. - -=== Locking Exceptions - -When defining unique types with <> (i.e. requesting that JanusGraph ensures uniqueness) it is likely to encounter locking exceptions of the type `PermanentLockingException` under concurrent modifications to the graph. - -Such exceptions are to be expected, since JanusGraph cannot know how to recover from a transactional state where an earlier read value has been modified by another transaction since this may invalidate the state of the transaction. In most cases it is sufficient to simply re-run the transaction. If locking exceptions are very frequent, try to analyze and remove the source of congestion. - - -=== Ghost Vertices - -When the same vertex is concurrently removed in one transaction and modified in another, both transactions will successfully commit on eventually consistent storage backends and the vertex will still exist with only the modified properties or edges. This is referred to as a ghost vertex. It is possible to guard against ghost vertices on eventually consistent backends using key <> but this is prohibitively expensive in most cases. A more scalable approach is to allow ghost vertices temporarily and clearing them out in regular time intervals. - -Another option is to detect them at read-time using the option `checkInternalVertexExistence()` documented in <>. - -=== Debug-level Logging Slows Execution - -When the log level is set to `DEBUG` JanusGraph produces *a lot* of logging output which is useful to understand how particular queries get compiled, optimized, and executed. However, the output is so large that it will impact the query performance noticeably. Hence, use `INFO` severity or higher for production systems or benchmarking. - -=== JanusGraph OutOfMemoryException or excessive Garbage Collection - -If you experience memory issues or excessive garbage collection while running JanusGraph it is likely that the caches are configured incorrectly. If the caches are too large, the heap may fill up with cache entries. Try reducing the size of the transaction level cache before tuning the database level cache, in particular if you have many concurrent transactions. See <> for more information. - -=== JAMM Warning Messages - -When launching JanusGraph with embedded Cassandra, the following warnings may be displayed: - -`958 [MutationStage:25] WARN org.apache.cassandra.db.Memtable - MemoryMeter uninitialized (jamm not specified as java agent); assuming liveRatio of 10.0. Usually this means cassandra-env.sh disabled jamm because you are using a buggy JRE; upgrade to the Sun JRE instead` - -Cassandra uses a Java agent called `MemoryMeter` which allows it to measure the actual memory use of an object, including JVM overhead. To use https://github.com/jbellis/jamm[JAMM] (Java Agent for Memory Measurements), the path to the JAMM jar must be specific in the Java javaagent parameter when launching the JVM (e.g. `-javaagent:path/to/jamm.jar`) through either janusgraph.sh, gremlin.sh, or Gremlin Server: - -[source, bash] -export JANUSGRAPH_JAVA_OPTS=-javaagent:$JANUSGRAPH_HOME/lib/jamm-$MAVEN{jamm.version}.jar - -=== Cassandra Connection Problem - -By default, JanusGraph uses the Astyanax library to connect to Cassandra clusters. On EC2 and Rackspace, it has been reported that Astyanax was unable to establish a connection to the cluster. In those cases, changing the backend to `storage.backend=cassandrathrift` solved the problem. - -=== Elasticsearch OutOfMemoryException - -When numerous clients are connecting to Elasticsearch, it is likely that an `OutOfMemoryException` occurs. This is not due to a memory issue, but to the OS not allowing more threads to be spawned by the user (the user running Elasticsearch). To circumvent this issue, increase the number of allowed processes to the user running Elasticsearch. For example, increase the `ulimit -u` from the default 1024 to 10024. - -=== Dropping a Database - -To drop a database using the Gremlin Console you can call `JanusGraphFactory.drop(graph)`. The graph you want to drop needs to be defined prior to running the drop method. - -With ConfiguredGraphFactory -[source, gremlin] ----- -graph = ConfiguredGraphFactory.open('example') -ConfiguredGraphFactory.drop('example'); ----- - -With JanusGraphFactory -[source, gremlin] ----- -graph = JanusGraphFactory.open('path/to/configuration.properties') -JanusGraphFactory.drop(graph); ----- - -Note that on JanusGraph versions prior to 0.3.0 if multiple Gremlin Server instances are connecting to the graph that has been dropped it is reccomended to close the graph on all active nodes by running either `JanusGraphFactory.close(graph)` or `ConfiguredGraphFactory.close("example")` depending on which graph manager is in use. Closing and reopening the graph on all active nodes will prevent cached(stale) references to the graph that has been dropped. ConfiguredGraphFactory graphs that are dropped may need to have their configurations recreated using the <> or <>. - -[[limitations]] -== Technical Limitations - -There are various limitations and "gotchas" that one should be aware -of when using JanusGraph. Some of these limitations are necessary design -choices and others are issues that will be rectified as JanusGraph -development continues. Finally, the last section provides solutions to -common issues. - -=== Design Limitations - -These limitations reflect long-term tradeoffs design tradeoffs which -are either difficult or impractical to change. These limitations are -unlikely to be removed in the near future. - -==== Size Limitation - -JanusGraph can store up to a quintillion edges (2^60) and half as many vertices. That limitation is imposed by JanusGraph's id scheme. - -==== DataType Definitions - -When declaring the data type of a property key using `dataType(Class)` JanusGraph will enforce that all properties for that key have the declared type, unless that type is `Object.class`. This is an equality type check, meaning that sub-classes will not be allowed. For instance, one cannot declare the data type to be `Number.class` and use `Integer` or `Long`. For efficiency reasons, the type needs to match exactly. Hence, use `Object.class` as the data type for type flexibility. In all other cases, declare the actual data type to benefit from increased performance and type safety. - -Edge Retrievals are O(log(k)) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Retrieving an edge by id, e.g `tx.getEdge(edge.getId())`, is not a constant time operation because it requires an index call on one of its adjacent vertices. Hence, the cost of retrieving an individual edge by its id is `O(log(k))` where `k` is the number of incident edges on the adjacent vertex. JanusGraph will attempt to pick the adjacent vertex with the smaller degree. - -This also applies to index retrievals for edges via a standard or external index. - -==== Type Definitions cannot be changed - -The definition of an edge label, property key, or vertex label cannot be changed once it has been committed to the graph. However, a type can be renamed and new types can be created at runtime to accommodate an evolving schema. - -==== Reserved Keywords - -There are certain keywords that JanusGraph uses internally for types that cannot be used otherwise. These types include vertex labels, edge labels, and property keys. The following are keywords that cannot be used: - -* vertex -* element -* edge -* property -* label -* key - -For example, if you attempt to create a vertex with the label of `property`, you will receive an exception regarding protected system types. - -=== Temporary Limitations - -These are limitations in JanusGraph's current implementation. These -limitations could reasonably be removed in upcoming versions of JanusGraph. - -==== Limited Mixed Index Support - -Mixed indexes only support a subset of the data types that JanusGraph supports. See <> for a current listing. Also, mixed indexes do not currently support property keys with SET or LIST cardinality. - -==== Batch Loading Speed - -JanusGraph provides a batch loading mode that can be enabled through the <>. However, this batch mode only facilitates faster loading into the storage backend, it does not use storage backend specific batch loading techniques that prepare the data in memory for disk storage. As such, batch loading in JanusGraph is currently slower than batch loading modes provided by single machine databases. <> contains information on speeding up batch loading in JanusGraph. - -Another limitation related to batch loading is the failure to load millions of edges into a single vertex at once or in a short time of period. Such *supernode loading* can fail for some storage backends. This limitation also applies to dense index entries. diff --git a/docs/static/images/advanced-scenario.svg b/docs/basics/advanced-scenario.svg similarity index 100% rename from docs/static/images/advanced-scenario.svg rename to docs/basics/advanced-scenario.svg diff --git a/docs/static/images/advanced-scenario.xml b/docs/basics/advanced-scenario.xml similarity index 100% rename from docs/static/images/advanced-scenario.xml rename to docs/basics/advanced-scenario.xml diff --git a/docs/basics/cache.md b/docs/basics/cache.md new file mode 100644 index 0000000000..6856134925 --- /dev/null +++ b/docs/basics/cache.md @@ -0,0 +1,166 @@ + +JanusGraph Cache +================ + +Caching +------- + +JanusGraph employs multiple layers of data caching to facilitate fast +graph traversals. The caching layers are listed here in the order they +are accessed from within a JanusGraph transaction. The closer the cache +is to the transaction, the faster the cache access and the higher the +memory footprint and maintenance overhead. + +Transaction-Level Caching +------------------------- + +Within an open transaction, JanusGraph maintains two caches: + +- Vertex Cache: Caches accessed vertices and their adjacency list (or + subsets thereof) so that subsequent access is significantly faster + within the same transaction. Hence, this cache speeds up iterative + traversals. + +- Index Cache: Caches the results for index queries so that subsequent + index calls can be served from memory instead of calling the index + backend and (usually) waiting for one or more network round trips. + +The size of both of those is determined by the *transaction cache size*. +The transaction cache size can be configured via `cache.tx-cache-size` +or on a per transaction basis by opening a transaction via the +transaction builder `graph.buildTransaction()` and using the +`setVertexCacheSize(int)` method. + +### Vertex Cache + +The vertex cache contains vertices and the subset of their adjacency +list that has been retrieved in a particular transaction. The maximum +number of vertices maintained in this cache is equal to the transaction +cache size. If the transaction workload is an iterative traversal, the +vertex cache will significantly speed it up. If the same vertex is not +accessed again in the transaction, the transaction level cache will make +no difference. + +Note, that the size of the vertex cache on heap is not only determined +by the number of vertices it may hold but also by the size of their +adjacency list. In other words, vertices with large adjacency lists +(i.e. many incident edges) will consume more space in this cache than +those with smaller lists. + +Furthermore note, that modified vertices are *pinned* in the cache, +which means they cannot be evicted since that would entail loosing their +changes. Therefore, transaction which contain a lot of modifications may +end up with a larger than configured vertex cache. + +### Index Cache + +The index cache contains the results of index queries executed in the +context of this transaction. Subsequent identical index calls will be +served from this cache and are therefore significantly cheaper. If the +same index call never occurs twice in the same transaction, the index +cache makes no difference. + +Each entry in the index cache is given a weight equal to +`2 + result set size` and the total weight of the cache will not exceed +half of the transaction cache size. + +Database Level Caching +---------------------- + +The database level cache retains adjacency lists (or subsets thereof) +across multiple transactions and beyond the duration of a single +transaction. The database level cache is shared by all transactions +across a database. It is more space efficient than the transaction level +caches but also slightly slower to access. In contrast to the +transaction level caches, the database level caches do not expire +immediately after closing a transaction. Hence, the database level cache +significantly speeds up graph traversals for read heavy workloads across +transactions. + +[Configuration Reference](configuration-reference.md) lists all of the configuration +options that pertain to JanusGraph’s database level cache. This page +attempts to explain their usage. + +Most importantly, the database level cache is disabled by default in the +current release version of JanusGraph. To enable it, set +`cache.db-cache=true`. + +### Cache Expiration Time + +The most important setting for performance and query behavior is the +cache expiration time which is configured via `cache.db-cache-time`. The +cache will hold graph elements for at most that many milliseconds. If an +element expires, the data will be re-read from the storage backend on +the next access. + +If there is only one JanusGraph instance accessing the storage backend +or if this instance is the only one modifying the graph, the cache +expiration can be set to 0 which disables cache expiration. This allows +the cache to hold elements indefinitely (unless they are evicted due to +space constraints or on update) which provides the best cache +performance. Since no other JanusGraph instance is modifying the graph, +there is no danger of holding on to stale data. + +If there are multiple JanusGraph instances accessing the storage +backend, the time should be set to the maximum time that can be allowed +between **another** JanusGraph instance modifying the graph and this +JanusGraph instance seeing the data. If any change should be immediately +visible to all JanusGraph instances, the database level cache should be +disabled in a distributed setup. However, for most applications it is +acceptable that a particular JanusGraph instance sees remote +modifications with some delay. The larger the maximally allowed delay, +the better the cache performance. Note, that a given JanusGraph instance +will always immediately see its own modifications to the graph +irrespective of the configured cache expiration time. + +### Cache Size + +The configuration option `cache.db-cache-size` controls how much heap +space JanusGraph’s database level cache is allowed to consume. The +larger the cache, the more effective it will be. However, large cache +sizes can lead to excessive GC and poor performance. + +The cache size can be configured as a percentage (expressed as a decimal +between 0 and 1) of the total heap space available to the JVM running +JanusGraph or as an absolute number of bytes. + +Note, that the cache size refers to the amount of heap space that is +exclusively occupied by the cache. JanusGraph’s other data structures +and each open transaction will occupy additional heap space. If +additional software layers are running in the same JVM, those may occupy +a significant amount of heap space as well (e.g. Gremlin Server, +embedded Cassandra, etc). Be conservative in your heap memory +estimation. Configuring a cache that is too large can lead to +out-of-memory exceptions and excessive GC. + +### Clean Up Wait Time + +When a vertex is locally modified (e.g. an edge is added) all of the +vertex’s related database level cache entries are marked as expired and +eventually evicted. This will cause JanusGraph to refresh the vertex’s +data from the storage backend on the next access and re-populate the +cache. + +However, when the storage backend is eventually consistent, the +modifications that triggered the eviction may not yet be visible. By +configuring `cache.db-cache-clean-wait`, the cache will wait for at +least this many milliseconds before repopulating the cache with the +entry retrieved from the storage backend. + +If JanusGraph runs locally or against a storage backend that guarantees +immediate visibility of modifications, this value can be set to 0. + +Storage Backend Caching +----------------------- + +Each storage backend maintains its own data caching layer. These caches +benefit from compression, data compactness, coordinated expiration and +are often maintained off heap which means that large caches can be used +without running into garbage collection issues. While these caches can +be significantly larger than the database level cache, they are also +slower to access. + +The exact type of caching and its properties depends on the particular +[storage backend](../storage-backend/index.md). Please refer to the respective +documentation for more information about the caching infrastructure and +how to optimize it. diff --git a/docs/basics/common-questions.md b/docs/basics/common-questions.md new file mode 100644 index 0000000000..5808a936c4 --- /dev/null +++ b/docs/basics/common-questions.md @@ -0,0 +1,152 @@ +Common Questions +================ + +Accidental type creation +------------------------ + +By default, JanusGraph will automatically create property keys and edge +labels when a new type is encountered. It is strongly encouraged that +users explicitly schemata as documented in [Schema and Data Modeling](schema.md) before loading any data and disable automatic type +creation by setting the option `schema.default = none`. + +Automatic type creation can cause problems in multi-threaded or highly +concurrent environments. Since JanusGraph needs to ensure that types are +unique, multiple attempts at creating the same type will lead to locking +or other exceptions. It is generally recommended to create all needed +types up front or in one batch when new property keys and edge labels +are needed. + +Custom Class Datatype +--------------------- + +JanusGraph supports arbitrary objects as attribute values on properties. +To use a custom class as data type in JanusGraph, either register a +custom serializer or ensure that the class has a no-argument constructor +and implements the `equals` method because JanusGraph will verify that +it can successfully de-/serialize objects of that class. Please see +[Datatype and Attribute Serializer Configuration](../advanced-topics/serializer.md) for more information. + +Transactional Scope for Edges +----------------------------- + +Edges should not be accessed outside the scope in which they were +originally created or retrieved. + +Locking Exceptions +------------------ + +When defining unique types with [locking enabled](../advanced-topics/eventual-consistency.md) +(i.e. requesting that JanusGraph ensures uniqueness) it is likely to +encounter locking exceptions of the type `PermanentLockingException` +under concurrent modifications to the graph. + +Such exceptions are to be expected, since JanusGraph cannot know how to +recover from a transactional state where an earlier read value has been +modified by another transaction since this may invalidate the state of +the transaction. In most cases it is sufficient to simply re-run the +transaction. If locking exceptions are very frequent, try to analyze and +remove the source of congestion. + +Ghost Vertices +-------------- + +When the same vertex is concurrently removed in one transaction and +modified in another, both transactions will successfully commit on +eventually consistent storage backends and the vertex will still exist +with only the modified properties or edges. This is referred to as a +ghost vertex. It is possible to guard against ghost vertices on +eventually consistent backends using key [uniqueness](#index-unique) but +this is prohibitively expensive in most cases. A more scalable approach +is to allow ghost vertices temporarily and clearing them out in regular +time intervals. + +Another option is to detect them at read-time using the option +`checkInternalVertexExistence()` documented in [Transaction Configuration](#tx-config). + +Debug-level Logging Slows Execution +----------------------------------- + +When the log level is set to `DEBUG` JanusGraph produces **a lot** of +logging output which is useful to understand how particular queries get +compiled, optimized, and executed. However, the output is so large that +it will impact the query performance noticeably. Hence, use `INFO` +severity or higher for production systems or benchmarking. + +JanusGraph OutOfMemoryException or excessive Garbage Collection +--------------------------------------------------------------- + +If you experience memory issues or excessive garbage collection while +running JanusGraph it is likely that the caches are configured +incorrectly. If the caches are too large, the heap may fill up with +cache entries. Try reducing the size of the transaction level cache +before tuning the database level cache, in particular if you have many +concurrent transactions. See [JanusGraph Cache](cache.md) for more +information. + +JAMM Warning Messages +--------------------- + +When launching JanusGraph with embedded Cassandra, the following +warnings may be displayed: + +`958 [MutationStage:25] WARN org.apache.cassandra.db.Memtable - MemoryMeter uninitialized (jamm not specified as java agent); assuming liveRatio of 10.0. Usually this means cassandra-env.sh disabled jamm because you are using a buggy JRE; upgrade to the Sun JRE instead` + +Cassandra uses a Java agent called `MemoryMeter` which allows it to +measure the actual memory use of an object, including JVM overhead. To +use [JAMM](https://github.com/jbellis/jamm) (Java Agent for Memory +Measurements), the path to the JAMM jar must be specific in the Java +javaagent parameter when launching the JVM (e.g. +`-javaagent:path/to/jamm.jar`) through either `janusgraph.sh`, +`gremlin.sh`, or Gremlin Server: + + export JANUSGRAPH_JAVA_OPTS=-javaagent:$JANUSGRAPH_HOME/lib/jamm-{{ jamm_version }}.jar + +Cassandra Connection Problem +---------------------------- + +By default, JanusGraph uses the Astyanax library to connect to Cassandra +clusters. On EC2 and Rackspace, it has been reported that Astyanax was +unable to establish a connection to the cluster. In those cases, +changing the backend to `storage.backend=cassandrathrift` solved the +problem. + +Elasticsearch OutOfMemoryException +---------------------------------- + +When numerous clients are connecting to Elasticsearch, it is likely that +an `OutOfMemoryException` occurs. This is not due to a memory issue, but +to the OS not allowing more threads to be spawned by the user (the user +running Elasticsearch). To circumvent this issue, increase the number of +allowed processes to the user running Elasticsearch. For example, +increase the `ulimit -u` from the default 1024 to 10024. + +Dropping a Database +------------------- + +To drop a database using the Gremlin Console you can call +`JanusGraphFactory.drop(graph)`. The graph you want to drop needs to be +defined prior to running the drop method. + +With ConfiguredGraphFactory +```groovy +graph = ConfiguredGraphFactory.open('example') +ConfiguredGraphFactory.drop('example'); +``` + +With JanusGraphFactory +```groovy +graph = JanusGraphFactory.open('path/to/configuration.properties') +JanusGraphFactory.drop(graph); +``` + +Note that on JanusGraph versions prior to 0.3.0 if multiple Gremlin +Server instances are connecting to the graph that has been dropped it is +reccomended to close the graph on all active nodes by running either +`JanusGraphFactory.close(graph)` or +`ConfiguredGraphFactory.close("example")` depending on which graph +manager is in use. Closing and reopening the graph on all active nodes +will prevent cached(stale) references to the graph that has been +dropped. ConfiguredGraphFactory graphs that are dropped may need to have +their configurations recreated using the [graph configuration singleton](configured-graph-factory.md#graph-configurations) or +[template configuration](configured-graph-factory.md#template-configuration). + diff --git a/docs/basics/configuration-reference.md b/docs/basics/configuration-reference.md new file mode 100644 index 0000000000..7c13612177 --- /dev/null +++ b/docs/basics/configuration-reference.md @@ -0,0 +1,67 @@ +Configuration Reference +======================= + +This section is the authoritative reference for JanusGraph configuration +options. It includes all options for storage and indexing backends that +are part of the official JanusGraph distribution. + +The table is automatically generated by traversing the keys and +namespaces in JanusGraph’s internal configuration management API. Hence, +the configuration options as listed on this page are synchronized with a +particular JanusGraph release. If a reference to a configuration option +in other parts of this documentation is in conflict with its +representation on this page, assume the version listed here to be +correct. + +Mutability Levels +----------------- + +Each configuration option has a certain mutability level that governs +whether and how it can be modified after the database is opened for the +first time. The following listing describes the mutability levels. + +* FIXED + + Once the database has been opened, these configuration options cannot be changed for the entire life of the database + +* GLOBAL\_OFFLINE + + These options can only be changed for the entire database cluster at once when all instances are shut down + +* GLOBAL + + These options can only be changed globally across the entire database cluster + +* MASKABLE + + These options are global but can be overwritten by a local configuration file + +* LOCAL + These options can only be provided through a local configuration file + +Refer to [Global Configuration](#configuration-global) for information +on how to change non-local configuration options. + +Umbrella Namespace +------------------ + +Namespaces marked with an asterisk are **umbrella namespaces** which +means that they can accommodate an arbitrary number of sub-namespaces - +each of which uniquely identified by its name. The configuration options +listed under an umbrella namespace apply only to those sub-namespaces. +Umbrella namespaces are used to configure multiple system components +that are of the same type and hence have the same configuration options. + +For example, the `log` namespace is an umbrella namespace because +JanusGraph can interface with multiple logging backends, such as the +`user` log, each of which has the same core set of configuration +options. To configure the send batch size of the `user` log to 100 +transaction changes, one would have to set the following option in the +configuration +```conf +log.user.send-batch-size = 100 +``` + +Configuration Namespaces and Options +------------------------------------ +{!basics/janusgraph-cfg.md!} \ No newline at end of file diff --git a/docs/basics/configuration.md b/docs/basics/configuration.md new file mode 100644 index 0000000000..216a68a2cb --- /dev/null +++ b/docs/basics/configuration.md @@ -0,0 +1,246 @@ +Configuration +============= + +A JanusGraph graph database cluster consists of one or multiple +JanusGraph instances. To open a JanusGraph instance, a configuration has +to be provided which specifies how JanusGraph should be set up. + +A JanusGraph configuration specifies which components JanusGraph should +use, controls all operational aspects of a JanusGraph deployment, and +provides a number of tuning options to get maximum performance from a +JanusGraph cluster. + +At a minimum, a JanusGraph configuration must define the persistence +engine that JanusGraph should use as a storage backend. +[Storage Backends](../storage-backend/index.md) lists all supported persistence engines and how +to configure them respectively. If advanced graph query support (e.g +full-text search, geo search, or range queries) is required an +additional indexing backend must be configured. See +[Index Backends](../index-backend/search-predicates.md) for details. If query performance is a concern, +then caching should be enabled. Cache configuration and tuning is +described in [JanusGraph Cache](#caching). + +Example Configurations +---------------------- + +Below are some example configuration files to demonstrate how to +configure the most commonly used storage backends, indexing systems, and +performance components. This covers only a tiny portion of the available +configuration options. Refer to [Configuration Reference](configuration-reference.md) +for the complete list of all options. + +### Cassandra+Elasticsearch + +Sets up JanusGraph to use the Cassandra persistence engine running +locally and a remote Elastic search indexing system: + +```conf +storage.backend=cql +storage.hostname=localhost + +index.search.backend=elasticsearch +index.search.hostname=100.100.101.1, 100.100.101.2 +index.search.elasticsearch.client-only=true +``` + +### HBase+Caching + +Sets up JanusGraph to use the HBase persistence engine running remotely +and uses JanusGraph’s caching component for better performance. +```conf +storage.backend=hbase +storage.hostname=100.100.101.1 +storage.port=2181 + +cache.db-cache = true +cache.db-cache-clean-wait = 20 +cache.db-cache-time = 180000 +cache.db-cache-size = 0.5 +``` + +### BerkeleyDB + +Sets up JanusGraph to use BerkeleyDB as an embedded persistence engine +with Elasticsearch as an embedded indexing system. +```conf +storage.backend=berkeleyje +storage.directory=/tmp/graph + +index.search.backend=elasticsearch +index.search.directory=/tmp/searchindex +index.search.elasticsearch.client-only=false +index.search.elasticsearch.local-mode=true +``` + +[Configuration Reference](configuration-reference.md) describes all of these +configuration options in detail. The `conf` directory of the JanusGraph +distribution contains additional configuration examples. + +### Further Examples + +There are several example configuration files in the `conf/` directory +that can be used to get started with JanusGraph quickly. Paths to these +files can be passed to `JanusGraphFactory.open(...)` as shown below: + +```groovy +// Connect to Cassandra on localhost using a default configuration +graph = JanusGraphFactory.open("conf/janusgraph-cql.properties") +// Connect to HBase on localhost using a default configuration +graph = JanusGraphFactory.open("conf/janusgraph-hbase.properties") +``` + +Using Configuration +------------------- + +How the configuration is provided to JanusGraph depends on the +instantiation mode. + +### JanusGraphFactory + +#### Gremlin Console + +The JanusGraph distribution contains a command line Gremlin Console +which makes it easy to get started and interact with JanusGraph. Invoke +`bin/gremlin.sh` (Unix/Linux) or `bin/gremlin.bat` (Windows) to start +the Console and then open a JanusGraph graph using the factory with the +configuration stored in an accessible properties configuration file: +```groovy +graph = JanusGraphFactory.open('path/to/configuration.properties') +``` + +#### JanusGraph Embedded + +JanusGraphFactory can also be used to open an embedded JanusGraph graph +instance from within a JVM-based user application. In that case, +JanusGraph is part of the user application and the application can call +upon JanusGraph directly through its public API. + +#### Short Codes + +If the JanusGraph graph cluster has been previously configured and/or +only the storage backend needs to be defined, JanusGraphFactory accepts +a colon-separated string representation of the storage backend name and +hostname or directory. +```groovy +graph = JanusGraphFactory.open('cql:localhost') +graph = JanusGraphFactory.open('berkeleyje:/tmp/graph') +``` + +### JanusGraph Server + +JanusGraph, by itself, is simply a set of jar files with no thread of +execution. There are two basic patterns for connecting to, and using a +JanusGraph database: + +1. JanusGraph can be used by embedding JanusGraph calls in a client + program where the program provides the thread of execution. + +2. JanusGraph packages a long running server process that, when + started, allows a remote client or logic running in a separate + program to make JanusGraph calls. This long running server process + is called **JanusGraph Server**. + +For the JanusGraph Server, JanusGraph uses [Gremlin Server](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/#gremlin-server) of the [Apache TinkerPop](http://tinkerpop.apache.org/) stack to service client requests. JanusGraph provides an out-of-the-box configuration for a quick start with JanusGraph Server, but the configuration can be changed to provide a wide range of server capabilities. + +Configuring JanusGraph Server is accomplished through a JanusGraph +Server yaml configuration file located in the ./conf/gremlin-server +directory in the JanusGraph distribution. To configure JanusGraph Server +with a graph instance (`JanusGraph`), the JanusGraph Server +configuration file requires the following settings: + +```yaml +... +graphs: { + graph: conf/janusgraph-berkeleyje.properties +} +plugins: + - janusgraph.imports +... +``` + +The entry for `graphs` defines the bindings to specific `JanusGraph` +configurations. In the above case it binds `graph` to a JanusGraph +configuration at `conf/janusgraph-berkeleyje.properties`. The `plugins` +entry enables the JanusGraph Gremlin Plugin, which enables auto-imports +of JanusGraph classes so that they can be referenced in remotely +submitted scripts. + +Learn more about configuring and using JanusGraph Server in [JanusGraph Server](server.md). + +#### Server Distribution + +The JanusGraph zip file contains a quick start server component that +helps make it easier to get started with Gremlin Server and JanusGraph. +Invoke `bin/janusgraph.sh start` to start Gremlin Server with Cassandra +and Elasticsearch. + +!!! note + For security reasons Elasticsearch and therefore `janusgraph.sh` must + be run under a non-root account + +Global Configuration +-------------------- + +JanusGraph distinguishes between local and global configuration options. +Local configuration options apply to an individual JanusGraph instance. +Global configuration options apply to all instances in a cluster. More +specifically, JanusGraph distinguishes the following five scopes for +configuration options: + +- **LOCAL**: These options only apply to an individual JanusGraph + instance and are specified in the configuration provided when + initializing the JanusGraph instance. + +- **MASKABLE**: These configuration options can be overwritten for an + individual JanusGraph instance by the local configuration file. If + the local configuration file does not specify the option, its value + is read from the global JanusGraph cluster configuration. + +- **GLOBAL**: These options are always read from the cluster + configuration and cannot be overwritten on an instance basis. + +- **GLOBAL\_OFFLINE**: Like *GLOBAL*, but changing these options + requires a cluster restart to ensure that the value is the same + across the entire cluster. + +- **FIXED**: Like *GLOBAL*, but the value cannot be changed once the + JanusGraph cluster is initialized. + +When the first JanusGraph instance in a cluster is started, the global +configuration options are initialized from the provided local +configuration file. Subsequently changing global configuration options +is done through JanusGraph’s management API. To access the management +API, call `g.getManagementSystem()` on an open JanusGraph instance +handle `g`. For example, to change the default caching behavior on a +JanusGraph cluster: +```groovy +mgmt = graph.openManagement() +mgmt.get('cache.db-cache') +// Prints the current config setting +mgmt.set('cache.db-cache', true) +// Changes option +mgmt.get('cache.db-cache') +// Prints 'true' +mgmt.commit() +// Changes take effect +``` + +### Changing Offline Options + +Changing configuration options does not affect running instances and +only applies to newly started ones. Changing *GLOBAL\_OFFLINE* +configuration options requires restarting the cluster so that the +changes take effect immediately for all instances. To change +*GLOBAL\_OFFLINE* options follow these steps: + +- Close all but one JanusGraph instance in the cluster +- Connect to the single instance +- Ensure all running transactions are closed +- Ensure no new transactions are started (i.e. the cluster must be offline) +- Open the management API +- Change the configuration option(s) +- Call commit which will automatically shut down the graph instance +- Restart all instances + +Refer to the full list of configuration options in [Configuration Reference](configuration-reference.md) for more information including the configuration +scope of each option. \ No newline at end of file diff --git a/docs/configuredgraphfactory.adoc b/docs/basics/configured-graph-factory.md similarity index 55% rename from docs/configuredgraphfactory.adoc rename to docs/basics/configured-graph-factory.md index 11fd045df0..afaf91c7e3 100644 --- a/docs/configuredgraphfactory.adoc +++ b/docs/basics/configured-graph-factory.md @@ -1,206 +1,212 @@ -[[configuredgraphfactory]] -== ConfiguredGraphFactory +ConfiguredGraphFactory +====================== -The JanusGraph Server can be configured to use the `ConfiguredGraphFactory`. -The `ConfiguredGraphFactory` is an access point to your graphs, similar -to the `JanusGraphFactory`. These graph factories provide methods for -dynamically managing the graphs hosted on the server. +The JanusGraph Server can be configured to use the +`ConfiguredGraphFactory`. The `ConfiguredGraphFactory` is an access +point to your graphs, similar to the `JanusGraphFactory`. These graph +factories provide methods for dynamically managing the graphs hosted on +the server. -[[overview]] -=== Overview +Overview +-------- `JanusGraphFactory` is a class that provides an access point to your -graphs by providing a Configuration object each time you access the graph. +graphs by providing a Configuration object each time you access the +graph. -`ConfiguredGraphFactory` provides an access point to your graphs for which -you have previously created configurations using the +`ConfiguredGraphFactory` provides an access point to your graphs for +which you have previously created configurations using the `ConfigurationManagementGraph`. It also offers an access point to manage graph configurations. -`ConfigurationManagementGraph` allows you to manage graph configurations. +`ConfigurationManagementGraph` allows you to manage graph +configurations. -`JanusGraphManager` is an internal server component that tracks -graph references, provided your graphs are configured to use it. +`JanusGraphManager` is an internal server component that tracks graph +references, provided your graphs are configured to use it. -[[configuredgraphfactory-versus-JanusGraphfactory]] -=== ConfiguredGraphFactory versus JanusGraphFactory +ConfiguredGraphFactory versus JanusGraphFactory +----------------------------------------------- However, there is an important distinction between these two graph factories: 1. The `ConfiguredGraphFactory` can only be used if you have configured -your server to use the `ConfigurationManagementGraph` APIs at server -start. + your server to use the `ConfigurationManagementGraph` APIs at server + start. The benefits of using the `ConfiguredGraphFactory` are that: 1. You only need to supply a `String` to access your graphs, as opposed -to the `JanusGraphFactory`-- which requires you to specify information -about the backend you wish to use when accessing a graph-- every time -you open a graph. + to the `JanusGraphFactory`-- which requires you to specify + information about the backend you wish to use when accessing a + graph-- every time you open a graph. -2. If your ConfigurationManagementGraph is configured with a distributed -storage backend then your graph configurations are available to all -JanusGraph nodes in your cluster. +2. If your ConfigurationManagementGraph is configured with a + distributed storage backend then your graph configurations are + available to all JanusGraph nodes in your cluster. -[[how-does-the-configuredgraphfactory-work]] -=== How Does the ConfiguredGraphFactory Work? +How Does the ConfiguredGraphFactory Work? +----------------------------------------- The `ConfiguredGraphFactory` provides an access point to graphs under two scenarios: 1. You have already created a configuration for your specific graph -object using the `ConfigurationManagementGraph#createConfiguration`. In -this scenario, your graph is opened using the previously created -configuration for this graph. + object using the `ConfigurationManagementGraph#createConfiguration`. + In this scenario, your graph is opened using the previously created + configuration for this graph. + 2. You have already created a template configuration using the -`ConfigurationManagementGraph#createTemplateConfiguration`. In this -scenario, we create a configuration for the graph you are creating by -copying over all attributes stored in your template configuration and -appending the relevant graphName attribute, and we then open the graph -according to that specific configuration. + `ConfigurationManagementGraph#createTemplateConfiguration`. In this + scenario, we create a configuration for the graph you are creating + by copying over all attributes stored in your template configuration + and appending the relevant graphName attribute, and we then open the + graph according to that specific configuration. -[[accessing-the-graphs]] -=== Accessing the Graphs +Accessing the Graphs +-------------------- -You can either use `ConfiguredGraphFactory.create("graphName")` -or `ConfiguredGraphFactory.open("graphName")`. Learn more about the difference -between these two options by reading the section below about the `ConfigurationManagementGraph`. +You can either use `ConfiguredGraphFactory.create("graphName")` or +`ConfiguredGraphFactory.open("graphName")`. Learn more about the +difference between these two options by reading the section below about +the `ConfigurationManagementGraph`. -[[listing-the-graphs]] -=== Listing the Graphs +Listing the Graphs +------------------ -`ConfiguredGraphFactory.getGraphNames()` will return a set of graph names -for which you have created configurations using the `ConfigurationManagementGraph` APIs. +`ConfiguredGraphFactory.getGraphNames()` will return a set of graph +names for which you have created configurations using the +`ConfigurationManagementGraph` APIs. -`JanusGraphFactory.getGraphNames()` on the other hand returns a set of graph names -for which you have instantiated _and_ the references are stored inside the `JanusGraphManager`. +`JanusGraphFactory.getGraphNames()` on the other hand returns a set of +graph names for which you have instantiated *and* the references are +stored inside the `JanusGraphManager`. -[[dropping-a-graph]] -=== Dropping a Graph +Dropping a Graph +---------------- -`ConfiguredGraphFactory.drop("graphName")` will drop the graph database, deleting all data in storage and indexing backends. The graph can be open or closed (will be closed as part of the drop operation). Furthermore, this will also remove any existing graph configuration in the `ConfigurationManagementGraph`. +`ConfiguredGraphFactory.drop("graphName")` will drop the graph database, +deleting all data in storage and indexing backends. The graph can be +open or closed (will be closed as part of the drop operation). +Furthermore, this will also remove any existing graph configuration in +the `ConfigurationManagementGraph`. -IMPORTANT: This is an irreversible operation that will delete all graph and index data. +!!! important + This is an irreversible operation that will delete all graph and index + data. -IMPORTANT: To ensure all graph representations are consistent across all JanusGraph nodes in your cluster, remove the graph from the `JanusGraphManager` graph reference tracker on all nodes in your cluster: `ConfiguredGraphFactory.close("graphName");`. +!!! important + To ensure all graph representations are consistent across all + JanusGraph nodes in your cluster, remove the graph from the `JanusGraphManager` + graph reference tracker on all nodes in your cluster: `ConfiguredGraphFactory.close("graphName");`. -[[configuring-JanusGraph-server-for-configuredgraphfactory]] -=== Configuring JanusGraph Server for ConfiguredGraphFactory +Configuring JanusGraph Server for ConfiguredGraphFactory +-------------------------------------------------------- To be able to use the `ConfiguredGraphFactory`, you must configure your server to use the `ConfigurationManagementGraph` APIs. To do this, you -have to inject a graph variable named "ConfigurationManagementGraph" in your -server's YAML's `graphs` map. For example: +have to inject a graph variable named "ConfigurationManagementGraph" in +your server’s YAML’s `graphs` map. For example: -[source, properties] ----- +```yaml graphManager: org.janusgraph.graphdb.management.JanusGraphManager graphs: { ConfigurationManagementGraph: conf/JanusGraph-configurationmanagement.properties } ----- +``` In this example, our `ConfigurationManagementGraph` graph will be configured using the properties stored inside -`conf/JanusGraph-configurationmanagement.properties`, which for -example, look like: +`conf/JanusGraph-configurationmanagement.properties`, which for example, +look like: -[source, properties] ----- +```conf gremlin.graph=org.janusgraph.core.ConfiguredGraphFactory storage.backend=cql graph.graphname=ConfigurationManagementGraph storage.hostname=127.0.0.1 ----- +``` Assuming the GremlinServer started successfully and the -`ConfigurationManagementGraph` was successfully instantiated, then all the -APIs available on the `ConfigurationManagementGraph` `Singleton` will -also act upon said graph. Furthermore, this is the graph that will be -used to access the configurations used to create/open graphs using the -`ConfiguredGraphFactory`. - -IMPORTANT: The `pom.xml` included in the JanusGraph distribution lists this dependency as -optional, but the `ConfiguredGraphFactory` makes use of the `JanusGraphManager`, -which requires a declared dependency on the `org.apache.tinkerpop:gremlin-server`. So -if you run into `NoClassDefFoundError` errors, then be sure to update according to this -message. - -[[configurationmanagementgraph]] -=== ConfigurationManagementGraph +`ConfigurationManagementGraph` was successfully instantiated, then all +the APIs available on the `ConfigurationManagementGraph` `Singleton` +will also act upon said graph. Furthermore, this is the graph that will +be used to access the configurations used to create/open graphs using +the `ConfiguredGraphFactory`. + +!!! important + The `pom.xml` included in the JanusGraph distribution lists this + dependency as optional, but the `ConfiguredGraphFactory` makes use of + the `JanusGraphManager`, which requires a declared dependency on the + `org.apache.tinkerpop:gremlin-server`. So if you run into + `NoClassDefFoundError` errors, then be sure to update according to + this message. + +ConfigurationManagementGraph +---------------------------- The `ConfigurationManagementGraph` is a `Singleton` that allows you to create/update/remove configurations that you can use to access your graphs using the `ConfiguredGraphFactory`. See above on configuring your server to enable use of these APIs. -IMPORTANT: The ConfiguredGraphFactory offers an access point to manage your -graph configurations managed by the `ConfigurationManagementGraph`, so instead -of acting upon the `Singleton` itself, you may act upon the corresponding -`ConfiguredGraphFactory` static methods. For example, you may use -`ConfiguredGraphFactory.removeTemplateConfiguration()` instead of -`ConfiguredGraphFactory.getInstance().removeTemplateConfiguration()`. +!!! important + The ConfiguredGraphFactory offers an access point to manage your graph + configurations managed by the `ConfigurationManagementGraph`, so + instead of acting upon the `Singleton` itself, you may act upon the + corresponding `ConfiguredGraphFactory` static methods. For example, + you may use `ConfiguredGraphFactory.removeTemplateConfiguration()` + instead of + `ConfiguredGraphFactory.getInstance().removeTemplateConfiguration()`. -[[graph-configurations]] -==== Graph Configurations +### Graph Configurations The `ConfigurationManagementGraph` singleton allows you to create configurations used to open specific graphs, referenced by the `graph.graphname` property. For example: -[source, gremlin] ----- +```groovy map = new HashMap(); map.put("storage.backend", "cql"); map.put("storage.hostname", "127.0.0.1"); map.put("graph.graphname", "graph1"); ConfiguredGraphFactory.createConfiguration(new MapConfiguration(map)); ----- +``` Then you could access this graph on any JanusGraph node using: - -[source, gremlin] ----- +```groovy ConfiguredGraphFactory.open("graph1"); ----- +``` -[[template-configuration]] -==== Template Configuration +### Template Configuration The `ConfigurationManagementGraph` also allows you to create one template configuration, which you can use to create many graphs using the same configuration template. For example: - -[source, gremlin] ----- +```groovy map = new HashMap(); map.put("storage.backend", "cql"); map.put("storage.hostname", "127.0.0.1"); ConfiguredGraphFactory.createTemplateConfiguration(new MapConfiguration(map)); ----- +``` After doing this, you can create graphs using the template configuration: - -[source, gremlin] ----- +```groovy ConfiguredGraphFactory.create("graph2"); ----- +``` This method will first create a new configuration for "graph2" by copying over all the properties associated with the template configuration and storing it on a configuration for this specific graph. This means that this graph can be accessed in, on any JanusGraph node, in the future by doing: - -[source, gremlin] ----- +```groovy ConfiguredGraphFactory.open("graph2"); ----- +``` -[[updating-configurations]] -==== Updating Configurations +### Updating Configurations All interactions with both the `JanusGraphFactory` and the `ConfiguredGraphFactory` that interact with configurations that define @@ -208,40 +214,34 @@ the property `graph.graphname` go through the `JanusGraphManager` which keeps track of graph references created on the given JVM. Think of it as a graph cache. For this reason: -IMPORTANT: Any updates to a configuration are not guaranteed to take effect until -you remove the graph in question on every JanusGraph node in your -cluster. +!!! important + IMPORTANT: Any updates to a configuration are not guaranteed to take effect until + you remove the graph in question on every JanusGraph node in your cluster. You can do so by calling: - -[source, gremlin] ----- +```groovy ConfiguredGraphFactory.close("graph2"); ----- +``` Since graphs created using the template configuration first create a configuration for that graph in question using a copy and create method, this means that: -[IMPORTANT] -==== -Any updates to a specific graph created using the template -configuration are not guaranteed to take effect on the specific graph -until: - -1. The relevant configuration is removed: `ConfiguredGraphFactory.removeConfiguration("graph2");` -2. The graph in question has been closed on every JanusGraph node: `ConfiguredGraphFactory.close("graph2");` -3. The graph is recreated using the template configuration: `ConfiguredGraphFactory.create("graph2");` -==== +!!! important + Any updates to a specific graph created using the template + configuration are not guaranteed to take effect on the specific graph + until: + 1. The relevant configuration is removed: + `ConfiguredGraphFactory.removeConfiguration("graph2");` + 2. The graph in question has been closed on every JanusGraph node: `ConfiguredGraphFactory.close("graph2");` + 2. The graph is recreated using the template configuration: + `ConfiguredGraphFactory.create("graph2");` -[[update-examples]] -==== Update Examples +### Update Examples -1) We migrated our Cassandra data to a new server with a new -IP address: +1) We migrated our Cassandra data to a new server with a new IP address: -[source, gremlin] ----- +```java map = new HashMap(); map.put("storage.backend", "cql"); map.put("storage.hostname", "127.0.0.1"); @@ -262,12 +262,10 @@ ConfiguredGraphFactory.close("graph1"); // We are now guaranteed to use the updated configuration g1 = ConfiguredGraphFactory.open("graph1"); ----- +``` 2) We added an Elasticsearch node to our setup: - -[source, gremlin] ----- +```java map = new HashMap(); map.put("storage.backend", "cql"); map.put("storage.hostname", "127.0.0.1"); @@ -285,17 +283,13 @@ map.put("index.search.elasticsearch.transport-scheme", "http"); ConfiguredGraphFactory.updateConfiguration("graph1", map); -// Close graph -ConfiguredGraphFactory.close("graph1"); - // We are now guaranteed to use the updated configuration g1 = ConfiguredGraphFactory.open("graph1"); ----- - -3) Update a graph configuration that was created using a template configuration that has been updated: +``` -[source, gremlin] ----- +3) Update a graph configuration that was created using a template +configuration that has been updated: +```java map = new HashMap(); map.put("storage.backend", "cql"); map.put("storage.hostname", "127.0.0.1"); @@ -321,57 +315,84 @@ ConfiguredGraphFactory.close("graph1"); // Recreate ConfiguredGraphFactory.create("graph1"); // Now this graph's configuration is guaranteed to be updated ----- +``` -[[JanusGraphmanager]] -=== JanusGraphManager +JanusGraphManager +----------------- -The `JanusGraphManager` is a `Singleton` adhering to the TinkerPop graphManager specifications. +The `JanusGraphManager` is a `Singleton` adhering to the TinkerPop +graphManager specifications. In particular, the `JanusGraphManager` provides: -1. a coordinated mechanism by which to instantiate graph references on a given JanusGraph node -2. a graph reference tracker (or cache) +1. a coordinated mechanism by which to instantiate graph references on + a given JanusGraph node -Any graph you create using the `graph.graphname` property will go through the `JanusGraphManager` and thus be instantiated in a coordinated fashion. The graph reference will also be placed in the graph cache on the JVM in question. +2. a graph reference tracker (or cache) -Thus, any graph you open using the `graph.graphname` property that has already been instantiated on the JVM in question will be retrieved from the graph cache. +Any graph you create using the `graph.graphname` property will go +through the `JanusGraphManager` and thus be instantiated in a +coordinated fashion. The graph reference will also be placed in the +graph cache on the JVM in question. -This is why updates to your configurations require a few steps to guarantee correctness. +Thus, any graph you open using the `graph.graphname` property that has +already been instantiated on the JVM in question will be retrieved from +the graph cache. -[[usingtheJanusGraphmanager]] -==== How To Use The JanusGraphManager +This is why updates to your configurations require a few steps to +guarantee correctness. +### How To Use The JanusGraphManager -This is a new configuration option you can use when defining a property in your configuration that defines how to access a graph. All configurations that include this property will result in the graph instantiation happening through the `JanusGraphManager` (process explained above). +This is a new configuration option you can use when defining a property +in your configuration that defines how to access a graph. All +configurations that include this property will result in the graph +instantiation happening through the `JanusGraphManager` (process +explained above). -For backwards compatibility, any graphs that do not supply this parameter but supplied at server start in your graphs object in your .yaml file, these graphs will be bound through the JanusGraphManager denoted by their `key` supplied for that graph. For example, if your .yaml graphs object looks like: +For backwards compatibility, any graphs that do not supply this +parameter but supplied at server start in your graphs object in your +.yaml file, these graphs will be bound through the JanusGraphManager +denoted by their `key` supplied for that graph. For example, if your +.yaml graphs object looks like: -[source, properties] ----- +```yaml graphManager: org.janusgraph.graphdb.management.JanusGraphManager graphs { graph1: conf/graph1.properties, graph2: conf/graph2.properties } ----- - -but `conf/graph1.properties` and `conf/graph2.properties` do not include the property `graph.graphname`, then these graphs will be stored in the JanusGraphManager and thus bound in your gremlin script executions as `graph1` and `graph2`, respectively. - - -[[important]] -==== Important - -For convenience, if your configuration used to open a graph specifies `graph.graphname`, but does not specify the backend's storage directory, tablename, or keyspacename, then the relevant parameter will automatically be set to the value of `graph.graphname`. However, if you supply one of those parameters, that value will always take precedence. And if you supply neither, they default to the configuration option's default value. - -One special case is `storage.root` configuration option. This is a new configuration option used to specify the base of the directory that will be used for any backend requiring local storage directory access. If you supply this parameter, you must also supply the `graph.graphname` property, and the absolute storage directory will be equal to the value of the `graph.graphname` property appended to the value of the `storage.root` property. +``` + +but `conf/graph1.properties` and `conf/graph2.properties` do not include +the property `graph.graphname`, then these graphs will be stored in the +JanusGraphManager and thus bound in your gremlin script executions as +`graph1` and `graph2`, respectively. + +### Important + +For convenience, if your configuration used to open a graph specifies +`graph.graphname`, but does not specify the backend’s storage directory, +tablename, or keyspacename, then the relevant parameter will +automatically be set to the value of `graph.graphname`. However, if you +supply one of those parameters, that value will always take precedence. +And if you supply neither, they default to the configuration option’s +default value. + +One special case is `storage.root` configuration option. This is a new +configuration option used to specify the base of the directory that will +be used for any backend requiring local storage directory access. If you +supply this parameter, you must also supply the `graph.graphname` +property, and the absolute storage directory will be equal to the value +of the `graph.graphname` property appended to the value of the +`storage.root` property. Below are some example use cases: -1) Create a template configuration for my Cassandra backend such that each graph created using this configuration gets a unique keyspace equivalent to the `String` provided to the factory: - -[source, gremlin] ----- +1) Create a template configuration for my Cassandra backend such that +each graph created using this configuration gets a unique keyspace +equivalent to the `String` <graphName> provided to the factory: +```groovy map = new HashMap(); map.put("storage.backend", "cql"); map.put("storage.hostname", "127.0.0.1"); @@ -381,12 +402,13 @@ MapConfiguration(map)); g1 = ConfiguredGraphFactory.create("graph1"); //keyspace === graph1 g2 = ConfiguredGraphFactory.create("graph2"); //keyspace === graph2 g3 = ConfiguredGraphFactory.create("graph3"); //keyspace === graph3 ----- +``` -2) Create a template configuration for my BerkeleyJE backend such that each graph created using this configuration gets a unique storage directory equivalent to the "/": - -[source, gremlin] ----- +2) Create a template configuration for my BerkeleyJE backend such that +each graph created using this configuration gets a unique storage +directory equivalent to the +"<storage.root>/<graph.graphname>": +```groovy map = new HashMap(); map.put("storage.backend", "berkeleyje"); map.put("storage.root", "/data/graphs"); @@ -396,15 +418,17 @@ MapConfiguration(map)); g1 = ConfiguredGraphFactory.create("graph1"); //storage directory === /data/graphs/graph1 g2 = ConfiguredGraphFactory.create("graph2"); //storage directory === /data/graphs/graph2 g3 = ConfiguredGraphFactory.create("graph3"); //storage directory === /data/graphs/graph3 ----- - -[[examples]] -=== Examples +``` -It is reccomended to use a sessioned connection when creating a Configured Graph Factory template. If a sessioned connection is not used the Configured Graph Factory Template creation must be sent to the server as a single line using semi-colons. See details on sessions can be found in <>. +Examples +-------- -[source, gremlin] ----- +It is reccomended to use a sessioned connection when creating a +Configured Graph Factory template. If a sessioned connection is not used +the Configured Graph Factory Template creation must be sent to the +server as a single line using semi-colons. See details on sessions can +be found in [Connecting to Gremlin Server](../connecting/index.md). +```groovy gremlin> :remote connect tinkerpop.server conf/remote.yaml session ==>Configured localhost/127.0.0.1:8182 @@ -468,4 +492,4 @@ Configuration for graph "graph2" already exists. gremlin> g2 = ConfiguredGraphFactory.open("graph2"); g2.vertices().size(); ==>1 ----- +``` \ No newline at end of file diff --git a/docs/basics/deployment.md b/docs/basics/deployment.md new file mode 100644 index 0000000000..30be24ff8f --- /dev/null +++ b/docs/basics/deployment.md @@ -0,0 +1,92 @@ +Deployment Scenarios +==================== + +JanusGraph offers a wide choice of storage and index backends which +results in great flexibility of how it can be deployed. This chapter +presents a few possible deployment scenarios to help with the complexity +that comes with this flexibility. + +Before discussing the different deployment scenarios, it is important to +understand the roles of JanusGraph itself and that of the backends. +First of all, applications only communicate directly with JanusGraph, +mostly by sending Gremlin traversals for execution. JanusGraph then +communicates with the configured backends to execute the received +traversal. When JanusGraph is used in the form of JanusGraph Server, +then there is nothing like a *master* JanusGraph Server. Applications +can therefore connect to any JanusGraph Server instance. They can also +use a load-balancer to schedule requests to the different instances. The +JanusGraph Server instances themselves don’t communicate to each other +directly which makes it easy to scale them when the need arises to +process more traversals. + +!!! note + The scenarios presented in this chapter are only examples of how + JanusGraph can be deployed. Each deployment needs to take into account + the concrete use cases and production needs. + +Getting Started Scenario +------------------------ + +This scenario is the scenario most users probably want to choose when +they are just getting started with JanusGraph. It offers scalability and +fault tolerance with a minimum number of servers required. JanusGraph +Server runs together with an instance of the storage backend and +optionally also an instance of the index backend on every server. + +![Getting started deployment scenario diagram](getting-started-scenario.svg) + +A setup like this can be extended by simply adding more servers of the +same kind or by moving one of the components onto dedicated servers. The +latter describes a growth path to transform the deployment into the +[Advanced Scenario](#advanced-scenario). + +Any of the scalable storage backends can be used with this scenario. +Note however that for Scylla [some configuration is required when it is +hosted co-located with other +services](http://docs.scylladb.com/getting-started/scylla_in_a_shared_environment/) +like in this scenario. When an index backend should be used in this +scenario then it also needs to be one that is scalable. + +Advanced Scenario +----------------- + +The advanced scenario is an evolution of the [Getting Started Scenario](#getting-started-scenario). + Instead of hosting the JanusGraph +Server instances together with the storage backend and optionally also +the index backend, they are now separated on different servers. The +advantage of hosting the different components (JanusGraph Server, +storage/index backend) on different servers is that they can be scaled +and managed independently of each other. This offers a higher +flexibility at the cost of having to maintain more servers. + +![Advanced deployment scenario diagram](advanced-scenario.svg) + +Since this scenario offers independent scalability of the different +components, it of course makes most sense to also use scalable backends. + +Minimalist Scenario +------------------- + +It is also possible to host JanusGraph Server together with the +backend(s) on just one server. This is especially attractive for testing +purposes or for example when JanusGraph just supports a single +application which can then also run on the same server. + +![Minimalist deployment scenario diagram](minimalist-scenario.svg) + +Opposed to the previous scenarios, it makes most sense to use backends +for this scenario that are not scalable. The in-memory backend can be +used for testing purposes or Berkeley DB for production and Lucene as +the optional index backend. + +Embedded JanusGraph +------------------- + +Instead of connecting to the JanusGraph Server from an application it is +also possible to embed JanusGraph as a library inside a JVM based +application. While this reduces the administrative overhead, it makes it +impossible to scale JanusGraph independently of the application. +Embedded JanusGraph can be deployed as a variation of any of the other +scenarios. JanusGraph just moves from the server(s) directly into the +application as its now just used as a library instead of an independent +service. diff --git a/docs/basics/example-config.md b/docs/basics/example-config.md new file mode 100644 index 0000000000..636833bec4 --- /dev/null +++ b/docs/basics/example-config.md @@ -0,0 +1,100 @@ +Example Graph Configuration +=========================== + +This page illustrates a number of common graph configurations. Please +refer to [Configuration Reference](configuration-reference.md) and the pages of the respective [storage backend](../storage-backend/index.md), [index backend](../index-backend/index.md) for more +information. + +Also, note that the JanusGraph distribution includes local configuration +files in the `conf/` directory. + +BerkeleyDB +---------- +```conf +storage.backend=berkeleyje +storage.directory=/tmp/graph + +index.search.backend=elasticsearch +index.search.directory=/tmp/searchindex +index.search.elasticsearch.client-only=false +index.search.elasticsearch.local-mode=true +``` + +This configuration file configures JanusGraph to use [BerkeleyDB](../storage-backend/bdb.md) +as an embedded storage backend, meaning, JanusGraph will start +BerkeleyDB internally. The primary data will be stored in the directory +`/tmp/graph`. + +In addition, this configures an embedded [Elasticsearch](../index-backend/elasticsearch.md) +index backend with the name `search`. JanusGraph will start +Elasticsearch internally and it will not be externally accessible since +`local-mode` is enabled. Elasticsearch stores all data for the `search` +index in `/tmp/searchindex`. Configuring an index backend is optional. + +Cassandra +--------- + +### Cassandra Remote + +```conf +storage.backend=cql +storage.hostname=100.100.100.1, 100.100.100.2 + +index.search.backend=elasticsearch +index.search.hostname=100.100.101.1, 100.100.101.2 +index.search.elasticsearch.client-only=true +``` + +This configuration file configures JanusGraph to use +[Cassandra](../storage-backend/cassandra.md) as a remote storage backend. It assumes that a +Cassandra cluster is running and accessible at the given IP addresses. +If Cassandra is running locally, use the IP address `127.0.0.1`. + +In addition, this configures a remote [Elasticsearch](../index-backend/elasticsearch.md) +index backend with the name `search`. It assumes that an Elasticsearch +cluster is running and accessible at the given IP addresses. Enabling +`client-only` ensures that the local instance does not join the existing +Elasticsearch cluster as another node but only connects to it. +Configuring an index backend is optional. + +### Embedded Cassandra + +```conf +storage.backend=embeddedcassandra +storage.conf-file=config/cassandra.yaml + +index.search.backend=elasticsearch +index.search.directory=/tmp/searchindex +index.search.elasticsearch.client-only=false +index.search.elasticsearch.local-mode=true +``` + +This configuration file configures JanusGraph to start +[Cassandra](../storage-backend/cassandra.md) internally embedded in JanusGraph and specifies +the yaml configuration file for Cassandra. Cassandra is still accessible +externally and can connect to other available Cassandra nodes to form a +cluster as configured in the yaml file. + +The optional index backend configuration is identical to embedded index +configuration described above. + +HBase +----- + +```conf +storage.backend=hbase +storage.hostname=127.0.0.1 +storage.port=2181 + +index.search.backend=elasticsearch +index.search.hostname=127.0.0.1 +index.search.elasticsearch.client-only=true +``` + +This configuration file configures JanusGraph to use [HBase](../storage-backend/hbase.md) as +a remote storage backend. It assumes that an HBase cluster is running +and accessible at the given IP addresses through the configured port. If +HBase is running locally, use the IP address `127.0.0.1`. + +The optional index backend configuration is identical to remote index +configuration described above. diff --git a/docs/static/images/getting-started-scenario.svg b/docs/basics/getting-started-scenario.svg similarity index 100% rename from docs/static/images/getting-started-scenario.svg rename to docs/basics/getting-started-scenario.svg diff --git a/docs/static/images/getting-started-scenario.xml b/docs/basics/getting-started-scenario.xml similarity index 100% rename from docs/static/images/getting-started-scenario.xml rename to docs/basics/getting-started-scenario.xml diff --git a/docs/basics/gremlin.md b/docs/basics/gremlin.md new file mode 100644 index 0000000000..eb80993ad9 --- /dev/null +++ b/docs/basics/gremlin.md @@ -0,0 +1,195 @@ +Gremlin Query Language +====================== + +![gremlin](http://tinkerpop.apache.org/docs/3.2.9/images/gremlin-logo.png) + +[Gremlin](http://tinkerpop.apache.org/gremlin.html) is JanusGraph’s +query language used to retrieve data from and modify data in the graph. +Gremlin is a path-oriented language which succinctly expresses complex +graph traversals and mutation operations. Gremlin is a [functional +language](http://en.wikipedia.org/wiki/Functional_programming) whereby +traversal operators are chained together to form path-like expressions. +For example, "from Hercules, traverse to his father and then his +father’s father and return the grandfather’s name." + +Gremlin is a component of [Apache +TinkerPop](http://tinkerpop.apache.org). It is developed independently +from JanusGraph and is supported by most graph databases. By building +applications on top of JanusGraph through the Gremlin query language, +users avoid vendor-lock in because their application can be migrated to +other graph databases supporting Gremlin. + +This section is a brief overview of the Gremlin query language. For more +information on Gremlin, refer to the following resources: + +- [Complete Gremlin Manual](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/): Reference manual for all of the Gremlin steps. + +- [Gremlin Console Tutorial](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/tutorials/the-gremlin-console/): Learn how to use the Gremlin Console effectively to traverse and analyze a graph interactively. + +- [Gremlin Recipes](tinkerpop.apache.org/docs/{{ tinkerpop_version }}/recipes/): A collection of best practices and common traversal patterns for Gremlin. + +- [Gremlin Language Drivers](http://tinkerpop.apache.org/index.html#language-drivers): + Connect to a Gremlin Server with different programming languages, + including Go, JavaScript, .NET/C\#, PHP, Python, Ruby, Scala, and + TypeScript. + +- [Gremlin Language Variants](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/tutorials/gremlin-language-variants/): Learn how to embed Gremlin in a host programming language. + +- [Gremlin for SQL developers](http://sql2gremlin.com): Learn Gremlin + using typical patterns found when querying data with SQL. + +In addition to these resources, [Connecting to JanusGraph](../connecting/index.md) explains how Gremlin +can be used in different programming languages to query a JanusGraph +Server. + +Introductory Traversals +----------------------- + +A Gremlin query is a chain of operations/functions that are evaluated +from left to right. A simple grandfather query is provided below over +the *Graph of the Gods* dataset discussed in [Getting Started](../index.md#getting-started). +```groovy +gremlin> g.V().has('name', 'hercules').out('father').out('father').values('name') +==>saturn +``` + +The query above can be read: + +1. `g`: for the current graph traversal. +2. `V`: for all vertices in the graph +3. `has('name', 'hercules')`: filters the vertices down to those with name property "hercules" (there is only one). +4. `out('father')`: traverse outgoing father edge’s from Hercules. +5. ‘out('father')\`: traverse outgoing father edge’s from Hercules’ father’s vertex (i.e. Jupiter). +6. `name`: get the name property of the "hercules" vertex’s grandfather. + +Taken together, these steps form a path-like traversal query. Each step +can be decomposed and its results demonstrated. This style of building +up a traversal/query is useful when constructing larger, complex query +chains. + +```groovy +gremlin> g +==>graphtraversalsource[janusgraph[cql:127.0.0.1], standard] +gremlin> g.V().has('name', 'hercules') +==>v[24] +gremlin> g.V().has('name', 'hercules').out('father') +==>v[16] +gremlin> g.V().has('name', 'hercules').out('father').out('father') +==>v[20] +gremlin> g.V().has('name', 'hercules').out('father').out('father').values('name') +==>saturn +``` + +For a sanity check, it is usually good to look at the properties of each +return, not the assigned long id. +```groovy +gremlin> g.V().has('name', 'hercules').values('name') +==>hercules +gremlin> g.V().has('name', 'hercules').out('father').values('name') +==>jupiter +gremlin> g.V().has('name', 'hercules').out('father').out('father').values('name') +==>saturn +``` + +Note the related traversal that shows the entire father family tree +branch of Hercules. This more complicated traversal is provided in order +to demonstrate the flexibility and expressivity of the language. A +competent grasp of Gremlin provides the JanusGraph user the ability to +fluently navigate the underlying graph structure. +```groovy +gremlin> g.V().has('name', 'hercules').repeat(out('father')).emit().values('name') +==>jupiter +==>saturn +``` + +Some more traversal examples are provided below. +```groovy +gremlin> hercules = g.V().has('name', 'hercules').next() +==>v[1536] +gremlin> g.V(hercules).out('father', 'mother').label() +==>god +==>human +gremlin> g.V(hercules).out('battled').label() +==>monster +==>monster +==>monster +gremlin> g.V(hercules).out('battled').valueMap() +==>{name=nemean} +==>{name=hydra} +==>{name=cerberus} +``` +Each step (denoted by a separating .) is a function that operates on the objects emitted from the previous step. There are numerous steps in the Gremlin language (see [Gremlin Steps](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/#graph-traversal-steps)). By simply changing a step or order of the steps, different traversal semantics are enacted. The example below returns the name of all the people that have battled the same monsters as Hercules who themselves are not Hercules (i.e. "co-battlers" or perhaps, "allies"). + +Given that *The Graph of the Gods* only has one battler (Hercules), +another battler (for the sake of example) is added to the graph with +Gremlin showcasing how vertices and edges are added to the graph. +```groovy +gremlin> theseus = graph.addVertex('human') +==>v[3328] +gremlin> theseus.property('name', 'theseus') +==>null +gremlin> cerberus = g.V().has('name', 'cerberus').next() +==>v[2816] +gremlin> battle = theseus.addEdge('battled', cerberus, 'time', 22) +==>e[7eo-2kg-iz9-268][3328-battled->2816] +gremlin> battle.values('time') +==>22 +``` + +When adding a vertex, an optional vertex label can be provided. An edge +label must be specified when adding edges. Properties as key-value pairs +can be set on both vertices and edges. When a property key is defined +with SET or LIST cardinality, `addProperty` must be used when adding a +respective property to a vertex. +```groovy +gremlin> g.V(hercules).as('h').out('battled').in('battled').where(neq('h')).values('name') +==>theseus +``` + +The example above has 4 chained functions: `out`, `in`, `except`, and +`values` (i.e. `name` is shorthand for `values('name')`). The function +signatures of each are itemized below, where `V` is vertex and `U` is +any object, where `V` is a subset of `U`. + +1. `out: V -> V` +2. `in: V -> V` +3. `except: U -> U` +4. `values: V -> U` + +When chaining together functions, the incoming type must match the +outgoing type, where `U` matches anything. Thus, the "co-battled/ally" +traversal above is correct. + +!!! note + The Gremlin overview presented in this section focused on the + Gremlin-Groovy language implementation used in the Gremlin Console. + Refer to [Connecting to JanusGraph](../connecting/index.md) for information about connecting to + JanusGraph with other languages than Groovy and independent of the + Gremlin Console. + +Iterating the Traversal +----------------------- + +One convenient feature of the Gremlin Console is that it automatically iterates all results from a query executed from the gremlin> prompt. This works well within the [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) environment as it shows you the results as a String. As you transition towards writing a Gremlin application, it is important to understand how to iterate a traversal explicitly because your application’s traversals will not iterate automatically. These are some of the common ways to iterate the [Traversal](http://tinkerpop.apache.org/javadocs/{{ tinkerpop_version }}/full/org/apache/tinkerpop/gremlin/process/traversal/Traversal.html): + +- `iterate()` - Zero results are expected or can be ignored. +- `next()` - Get one result. Make sure to check `hasNext()` first. +- `next(int n)` - Get the next `n` results. Make sure to check `hasNext()` first. +- `toList()` - Get all results as a list. If there are no results, an empty list is returned. + +A Java code example is shown below to demonstrate these concepts: +```java +Traversal t = g.V().has("name", "pluto"); // Define a traversal +// Note the traversal is not executed/iterated yet +Vertex pluto = null; +if (t.hasNext()) { // Check if results are available + pluto = g.V().has("name", "pluto").next(); // Get one result + g.V(pluto).drop().iterate(); // Execute a traversal to drop pluto from graph +} +// Note the traversal can be cloned for reuse +Traversal tt = t.asAdmin().clone(); +if (tt.hasNext()) { + System.err.println("pluto was not dropped!"); +} +List gods = g.V().hasLabel("god").toList(); // Find all the gods +``` \ No newline at end of file diff --git a/docs/basics/index-performance.md b/docs/basics/index-performance.md new file mode 100644 index 0000000000..13b9734f94 --- /dev/null +++ b/docs/basics/index-performance.md @@ -0,0 +1,488 @@ + +Indexing for Better Performance +=============================== + +JanusGraph supports two different kinds of indexing to speed up query +processing: **graph indexes** and **vertex-centric indexes**. Most graph +queries start the traversal from a list of vertices or edges that are +identified by their properties. Graph indexes make these global +retrieval operations efficient on large graphs. Vertex-centric indexes +speed up the actual traversal through the graph, in particular when +traversing through vertices with many incident edges. + +Graph Index +----------- + +Graph indexes are global index structures over the entire graph which +allow efficient retrieval of vertices or edges by their properties for +sufficiently selective conditions. For instance, consider the following +queries +```java +g.V().has('name', 'hercules') +g.E().has('reason', textContains('loves')) +``` + +The first query asks for all vertices with the name `hercules`. The +second asks for all edges where the property reason contains the word +`loves`. Without a graph index answering those queries would require a +full scan over all vertices or edges in the graph to find those that +match the given condition which is very inefficient and infeasible for +huge graphs. + +JanusGraph distinguishes between two types of graph indexes: +**composite** and **mixed** indexes. Composite indexes are very fast and +efficient but limited to equality lookups for a particular, +previously-defined combination of property keys. Mixed indexes can be +used for lookups on any combination of indexed keys and support multiple +condition predicates in addition to equality depending on the backing +index store. + +Both types of indexes are created through the JanusGraph management +system and the index builder returned by +`JanusGraphManagement.buildIndex(String, Class)` where the first +argument defines the name of the index and the second argument specifies +the type of element to be indexed (e.g. `Vertex.class`). The name of a +graph index must be unique. Graph indexes built against newly defined +property keys, i.e. property keys that are defined in the same +management transaction as the index, are immediately available. The same +applies to graph indexes that are constrained to a label that is created +in the same management transaction as the index. Graph indexes built +against property keys that are already in use without being constrained +to a newly created label require the execution of a [reindex procedure](../advanced-topics/index-admin.md) to ensure that the index contains all previously +added elements. Until the reindex procedure has completed, the index +will not be available. It is encouraged to define graph indexes in the +same transaction as the initial schema. + +!!! note + In the absence of an index, JanusGraph will default to a full graph + scan in order to retrieve the desired list of vertices. While this + produces the correct result set, the graph scan can be very + inefficient and lead to poor overall system performance in a + production environment. Enable the `force-index` configuration option + in production deployments of JanusGraph to prohibit graph scans. + +### Composite Index + +Composite indexes retrieve vertices or edges by one or a (fixed) +composition of multiple keys. Consider the following composite index +definitions. +```java +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +name = mgmt.getPropertyKey('name') +age = mgmt.getPropertyKey('age') +mgmt.buildIndex('byNameComposite', Vertex.class).addKey(name).buildCompositeIndex() +mgmt.buildIndex('byNameAndAgeComposite', Vertex.class).addKey(name).addKey(age).buildCompositeIndex() +mgmt.commit() +//Wait for the index to become available +ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call() +ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get() +mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +First, two property keys `name` and `age` are already defined. Next, a +simple composite index on just the name property key is built. +JanusGraph will use this index to answer the following query. +```groovy +g.V().has('name', 'hercules') +``` + +The second composite graph index includes both keys. JanusGraph will use +this index to answer the following query. +```groovy +g.V().has('age', 30).has('name', 'hercules') +``` + +Note, that all keys of a composite graph index must be found in the +query’s equality conditions for this index to be used. For example, the +following query cannot be answered with either of the indexes because it +only contains a constraint on `age` but not `name`. +```groovy +g.V().has('age', 30) +``` + +Also note, that composite graph indexes can only be used for equality +constraints like those in the queries above. The following query would +be answered with just the simple composite index defined on the `name` +key because the age constraint is not an equality constraint. +```java +g.V().has('name', 'hercules').has('age', inside(20, 50)) +``` + +Composite indexes do not require configuration of an external indexing +backend and are supported through the primary storage backend. Hence, +composite index modifications are persisted through the same transaction +as graph modifications which means that those changes are atomic and/or +consistent if the underlying storage backend supports atomicity and/or +consistency. + +!!! note + A composite index may comprise just one or multiple keys. A composite + index with just one key is sometimes referred to as a key-index. + +#### Index Uniqueness + +Composite indexes can also be used to enforce property uniqueness in the +graph. If a composite graph index is defined as `unique()` there can be +at most one vertex or edge for any given concatenation of property +values associated with the keys of that index. For instance, to enforce +that names are unique across the entire graph the following composite +graph index would be defined. +```groovy +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +name = mgmt.getPropertyKey('name') +mgmt.buildIndex('byNameUnique', Vertex.class).addKey(name).unique().buildCompositeIndex() +mgmt.commit() +//Wait for the index to become available +ManagementSystem.awaitGraphIndexStatus(graph, 'byNameUnique').call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("byNameUnique"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +!!! note + To enforce uniqueness against an eventually consistent storage + backend, the [consistency](../advanced-topics/eventual-consistency.md) of the index must be + explicitly set to enabling locking. + +### Mixed Index + +Mixed indexes retrieve vertices or edges by any combination of +previously added property keys. Mixed indexes provide more flexibility +than composite indexes and support additional condition predicates +beyond equality. On the other hand, mixed indexes are slower for most +equality queries than composite indexes. + +Unlike composite indexes, mixed indexes require the configuration of an +[indexing backend](../index-backend/search-predicates.md) and use that indexing backend to +execute lookup operations. JanusGraph can support multiple indexing +backends in a single installation. Each indexing backend must be +uniquely identified by name in the JanusGraph configuration which is +called the **indexing backend name**. +```groovy +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +name = mgmt.getPropertyKey('name') +age = mgmt.getPropertyKey('age') +mgmt.buildIndex('nameAndAge', Vertex.class).addKey(name).addKey(age).buildMixedIndex("search") +mgmt.commit() +//Wait for the index to become available +ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +The example above defines a mixed index containing the property keys +`name` and `age`. The definition refers to the indexing backend name +`search` so that JanusGraph knows which configured indexing backend it +should use for this particular index. The `search` parameter specified +in the buildMixedIndex call must match the second clause in the +JanusGraph configuration definition like this: index.**search**.backend +If the index was named *solrsearch* then the configuration definition +would appear like this: index.**solrsearch**.backend. + +The mgmt.buildIndex example specified above uses text search as its +default behavior. An index statement that explicitly defines the index +as a text index can be written as follows: +```groovy +mgmt.buildIndex('nameAndAge',Vertex.class).addKey(name,Mapping.TEXT.getParameter()).addKey(age,Mapping.TEXT.getParameter()).buildMixedIndex("search") +``` + +See [Index Parameters and Full-Text Search](../index-backend/text-search.md) for more information on text and string +search options, and see the documentation section specific to the +indexing backend in use for more details on how each backend handles +text versus string searches. + +While the index definition example looks similar to the composite index +above, it provides greater query support and can answer *any* of the +following queries. +```groovy +g.V().has('name', textContains('hercules')).has('age', inside(20, 50)) +g.V().has('name', textContains('hercules')) +g.V().has('age', lt(50)) +``` + +Mixed indexes support full-text search, range search, geo search and +others. Refer to [Search Predicates and Data Types](../index-backend/search-predicates.md) for a list of predicates +supported by a particular indexing backend. + +!!! note + Unlike composite indexes, mixed indexes do not support uniqueness. + +#### Adding Property Keys + +Property keys can be added to an existing mixed index which allows +subsequent queries to include this key in the query condition. +```groovy +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +location = mgmt.makePropertyKey('location').dataType(Geoshape.class).make() +nameAndAge = mgmt.getGraphIndex('nameAndAge') +mgmt.addIndexKey(nameAndAge, location) +mgmt.commit() +//Previously created property keys already have the status ENABLED, but +//our newly created property key "location" needs to REGISTER so we wait for both statuses +ManagementSystem.awaitGraphIndexStatus(graph, 'nameAndAge').status(SchemaStatus.REGISTERED, SchemaStatus.ENABLED).call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("nameAndAge"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +To add a newly defined key, we first retrieve the existing index from +the management transaction by its name and then invoke the `addIndexKey` +method to add the key to this index. + +If the added key is defined in the same management transaction, it will +be immediately available for querying. If the property key has already +been in use, adding the key requires the execution of a [reindex procedure](../advanced-topics/index-admin.md) to ensure that the index contains all previously +added elements. Until the reindex procedure has completed, the key will +not be available in the mixed index. + +#### Mapping Parameters + +When adding a property key to a mixed index - either through the index +builder or the `addIndexKey` method - a list of parameters can be +optionally specified to adjust how the property value is mapped into the +indexing backend. Refer to the [mapping parameters +overview](../index-backend/text-search.md) for a complete list of parameter types supported +by each indexing backend. + +### Ordering + +The order in which the results of a graph query are returned can be +defined using the `order().by()` directive. The `order().by()` method +expects two parameters: + +- The name of the property key by which to order the results. The + results will be ordered by the value of the vertices or edges for + this property key. + +- The sort order: either increasing `incr` or decreasing `decr` + +For example, the query +`g.V().has('name', textContains('hercules')).order().by('age', decr).limit(10)` +retrieves the ten oldest individuals with *hercules* in their name. + +When using `order().by()` it is important to note that: + +- Composite graph indexes do not natively support ordering search + 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, + 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. + +### Label Constraint + +In many cases it is desirable to only index vertices or edges with a +particular label. For instance, one may want to index only gods by their +name and not every single vertex that has a name property. When defining +an index it is possible to restrict the index to a particular vertex or +edge label using the `indexOnly` method of the index builder. The +following creates a composite index for the property key `name` that +indexes only vertices labeled `god`. +```groovy +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +name = mgmt.getPropertyKey('name') +god = mgmt.getVertexLabel('god') +mgmt.buildIndex('byNameAndLabel', Vertex.class).addKey(name).indexOnly(god).buildCompositeIndex() +mgmt.commit() +//Wait for the index to become available +ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndLabel').call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("byNameAndLabel"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +Label restrictions similarly apply to mixed indexes. When a composite +index with label restriction is defined as unique, the uniqueness +constraint only applies to properties on vertices or edges for the +specified label. + +### Composite versus Mixed Indexes + +1. Use a composite index for exact match index retrievals. Composite + indexes do not require configuring or operating an external index + system and are often significantly faster than mixed indexes. + + 1. As an exception, use a mixed index for exact matches when the + number of distinct values for query constraint is relatively + small or if one value is expected to be associated with many + elements in the graph (i.e. in case of low selectivity). + +2. Use a mixed indexes for numeric range, full-text or geo-spatial + indexing. Also, using a mixed index can speed up the order().by() + queries. + +Vertex-centric Indexes +---------------------- + +Vertex-centric indexes are local index structures built individually per +vertex. In large graphs vertices can have thousands of incident edges. +Traversing through those vertices can be very slow because a large +subset of the incident edges has to be retrieved and then filtered in +memory to match the conditions of the traversal. Vertex-centric indexes +can speed up such traversals by using localized index structures to +retrieve only those edges that need to be traversed. + +Suppose that Hercules battled hundreds of monsters in addition to the +three captured in the introductory [Graph of the Gods](../index.md#getting-started). Without a vertex-centric index, a query asking +for those monsters battled between time point `10` and `20` would +require retrieving all `battled` edges even though there are only a +handful of matching edges. +```groovy +h = g.V().has('name', 'hercules').next() +g.V(h).outE('battled').has('time', inside(10, 20)).inV() +``` + +Building a vertex-centric index by time speeds up such traversal +queries. Note, this initial index example already exists in the *Graph +of the Gods* as an index named `edges`. As a result, running the steps +below will result in a uniqueness constraint error. +```groovy +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +time = mgmt.getPropertyKey('time') +battled = mgmt.getEdgeLabel('battled') +mgmt.buildEdgeIndex(battled, 'battlesByTime', Direction.BOTH, Order.decr, time) +mgmt.commit() +//Wait for the index to become available +ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByTime').call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getGraphIndex("battlesByTime"), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +This example builds a vertex-centric index which indexes `battled` edges +in both direction by time in decreasing order. A vertex-centric index is +built against a particular edge label which is the first argument to the +index construction method `JanusGraphManagement.buildEdgeIndex()`. The +index only applies to edges of this label - `battled` in the example +above. The second argument is a unique name for the index. The third +argument is the edge direction in which the index is built. The index +will only apply to traversals along edges in this direction. In this +example, the vertex-centric index is built in both direction which means +that time restricted traversals along `battled` edges can be served by +this index in both the `IN` and `OUT` direction. JanusGraph will +maintain a vertex-centric index on both the in- and out-vertex of +`battled` edges. Alternatively, one could define the index to apply to +the `OUT` direction only which would speed up traversals from Hercules +to the monsters but not in the reverse direction. This would only +require maintaining one index and hence half the index maintenance and +storage cost. The last two arguments are the sort order of the index and +a list of property keys to index by. The sort order is optional and +defaults to ascending order (i.e. `Order.ASC`). The list of property +keys must be non-empty and defines the keys by which to index the edges +of the given label. A vertex-centric index can be defined with multiple +keys. +```groovy +graph.tx().rollback() //Never create new indexes while a transaction is active +mgmt = graph.openManagement() +time = mgmt.getPropertyKey('time') +rating = mgmt.makePropertyKey('rating').dataType(Double.class).make() +battled = mgmt.getEdgeLabel('battled') +mgmt.buildEdgeIndex(battled, 'battlesByRatingAndTime', Direction.OUT, Order.decr, rating, time) +mgmt.commit() +//Wait for the index to become available +ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByRatingAndTime', 'battled').call() +//Reindex the existing data +mgmt = graph.openManagement() +mgmt.updateIndex(mgmt.getRelationIndex(battled, 'battlesByRatingAndTime'), SchemaAction.REINDEX).get() +mgmt.commit() +``` + +This example extends the schema by a `rating` property on `battled` +edges and builds a vertex-centric index which indexes `battled` edges in +the out-going direction by rating and time in decreasing order. Note, +that the order in which the property keys are specified is important +because vertex-centric indexes are prefix indexes. This means, that +`battled` edges are indexed by `rating` *first* and `time` *second*. +```groovy +h = g.V().has('name', 'hercules').next() +g.V(h).outE('battled').property('rating', 5.0) //Add some rating properties +g.V(h).outE('battled').has('rating', gt(3.0)).inV() +g.V(h).outE('battled').has('rating', 5.0).has('time', inside(10, 50)).inV() +g.V(h).outE('battled').has('time', inside(10, 50)).inV() +``` + +Hence, the `battlesByRatingAndTime` index can speed up the first two but +not the third query. + +Multiple vertex-centric indexes can be built for the same edge label in +order to support different constraint traversals. JanusGraph’s query +optimizer attempts to pick the most efficient index for any given +traversal. Vertex-centric indexes only support equality and +range/interval constraints. + +!!! note + The property keys used in a vertex-centric index must have an + explicitly defined data type (i.e. *not* `Object.class`) which + supports a native sort order. + +If the vertex-centric index is built against an edge label that is +defined in the same management transaction, the index will be +immediately available for querying. If the edge label has already been +in use, building a vertex-centric index against it requires the +execution of a [reindex procedure](../advanced-topics/index-admin.md) to ensure that the index +contains all previously added edges. Until the reindex procedure has +completed, the index will not be available. + +!!! note + JanusGraph automatically builds vertex-centric indexes per edge label + and property key. That means, even with thousands of incident + `battled` edges, queries like `g.V(h).out('mother')` or + `g.V(h).values('age')` are efficiently answered by the local index. + +Vertex-centric indexes cannot speed up unconstrained traversals which +require traversing through all incident edges of a particular label. +Those traversals will become slower as the number of incident edges +increases. Often, such traversals can be rewritten as constrained +traversals that can utilize a vertex-centric index to ensure acceptable +performance at scale. + +### Ordered Traversals + +The following queries specify an order in which the incident edges are +to be traversed. Use the `localLimit` command to retrieve a subset of +the edges (in a given order) for EACH vertex that is traversed. +```groovy +h = g..V().has('name', 'hercules').next() +g.V(h).local(outE('battled').order().by('time', decr).limit(10)).inV().values('name') +g.V(h).local(outE('battled').has('rating', 5.0).order().by('time', decr).limit(10)).values('place') +``` + +The first query asks for the names of the 10 most recently battled +monsters by Hercules. The second query asks for the places of the 10 +most recent battles of Hercules that are rated 5 stars. In both cases, +the query is constrained by an order on a property key with a limit on +the number of elements to be returned. + +Such queries can also be efficiently answered by vertex-centric indexes +if the order key matches the key of the index and the requested order +(i.e. increasing or decreasing) is the same as the one defined for the +index. The `battlesByTime` index would be used to answer the first query +and `battlesByRatingAndTime` applies to the second. Note, that the +`battlesByRatingAndTime` index cannot be used to answer the first query +because an equality constraint on `rating` must be present for the +second key in the index to be effective. + +!!! note + Ordered vertex queries are a JanusGraph extension to Gremlin which + causes the verbose syntax and requires the `_()` step to convert the + JanusGraph result back into a Gremlin pipeline. diff --git a/docs/basics/janusgraph-cfg.md b/docs/basics/janusgraph-cfg.md new file mode 100644 index 0000000000..a1752c0bdf --- /dev/null +++ b/docs/basics/janusgraph-cfg.md @@ -0,0 +1,470 @@ + +### attributes.custom * +Custom attribute serialization and handling + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| attributes.custom.[X].attribute-class | Class of the custom attribute to be registered | String | (no default value) | GLOBAL_OFFLINE | +| attributes.custom.[X].serializer-class | Class of the custom attribute serializer to be registered | String | (no default value) | GLOBAL_OFFLINE | + +### cache +Configuration options that modify JanusGraph's caching behavior + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| cache.db-cache | Whether to enable JanusGraph's database-level cache, which is shared across all transactions. Enabling this option speeds up traversals by holding hot graph elements in memory, but also increases the likelihood of reading stale data. Disabling it forces each transaction to independently fetch graph elements from storage before reading/writing them. | Boolean | false | MASKABLE | +| cache.db-cache-clean-wait | How long, in milliseconds, database-level cache will keep entries after flushing them. This option is only useful on distributed storage backends that are capable of acknowledging writes without necessarily making them immediately visible. | Integer | 50 | GLOBAL_OFFLINE | +| cache.db-cache-size | Size of JanusGraph's database level cache. Values between 0 and 1 are interpreted as a percentage of VM heap, while larger values are interpreted as an absolute size in bytes. | Double | 0.3 | MASKABLE | +| cache.db-cache-time | Default expiration time, in milliseconds, for entries in the database-level cache. Entries are evicted when they reach this age even if the cache has room to spare. Set to 0 to disable expiration (cache entries live forever or until memory pressure triggers eviction when set to 0). | Long | 10000 | GLOBAL_OFFLINE | +| cache.tx-cache-size | Maximum size of the transaction-level cache of recently-used vertices. | Integer | 20000 | MASKABLE | +| cache.tx-dirty-size | Initial size of the transaction-level cache of uncommitted dirty vertices. This is a performance hint for write-heavy, performance-sensitive transactional workloads. If set, it should roughly match the median vertices modified per transaction. | Integer | (no default value) | MASKABLE | + +### cluster +Configuration options for multi-machine deployments + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| cluster.max-partitions | The number of virtual partition blocks created in the partitioned graph. This should be larger than the maximum expected number of nodesin the JanusGraph graph cluster. Must be bigger than 1 and a power of 2. | Integer | 32 | FIXED | + +### computer +GraphComputer related configuration + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| computer.result-mode | How the graph computer should return the computed results. 'persist' for writing them into the graph, 'localtx' for writing them into the local transaction, or 'none' (default) | String | none | MASKABLE | + +### graph +General configuration options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| graph.allow-stale-config | Whether to allow the local and storage-backend-hosted copies of the configuration to contain conflicting values for options with any of the following types: FIXED, GLOBAL_OFFLINE, GLOBAL. These types are managed globally through the storage backend and cannot be overridden by changing the local configuration. This type of conflict usually indicates misconfiguration. When this option is true, JanusGraph will log these option conflicts, but continue normal operation using the storage-backend-hosted value for each conflicted option. When this option is false, JanusGraph will log these option conflicts, but then it will throw an exception, refusing to start. | Boolean | true | MASKABLE | +| graph.graphname | This config option is an optional configuration setting that you may supply when opening a graph. The String value you provide will be the name of your graph. If you use the ConfigurationManagament APIs, then you will be able to access your graph by this String representation using the ConfiguredGraphFactory APIs. | String | (no default value) | LOCAL | +| graph.replace-instance-if-exists | If a JanusGraph instance with the same instance identifier already exists, the usage of this configuration option results in the opening of this graph anwyay. | Boolean | false | LOCAL | +| graph.set-vertex-id | Whether user provided vertex ids should be enabled and JanusGraph's automatic id allocation be disabled. Useful when operating JanusGraph in concert with another storage system that assigns long ids but disables some of JanusGraph's advanced features which can lead to inconsistent data. EXPERT FEATURE - USE WITH GREAT CARE. | Boolean | false | FIXED | +| graph.timestamps | The timestamp resolution to use when writing to storage and indices. Sets the time granularity for the entire graph cluster. To avoid potential inaccuracies, the configured time resolution should match those of the backend systems. Some JanusGraph storage backends declare a preferred timestamp resolution that reflects design constraints in the underlying service. When the backend provides a preferred default, and when this setting is not explicitly declared in the config file, the backend default is used and the general default associated with this setting is ignored. An explicit declaration of this setting overrides both the general and backend-specific defaults. | TimestampProviders | MICRO | FIXED | +| graph.unique-instance-id | Unique identifier for this JanusGraph instance. This must be unique among all instances concurrently accessing the same stores or indexes. It's automatically generated by concatenating the hostname, process id, and a static (process-wide) counter. Leaving it unset is recommended. | String | (no default value) | LOCAL | +| graph.unique-instance-id-suffix | When this is set and unique-instance-id is not, this JanusGraph instance's unique identifier is generated by concatenating the hex encoded hostname to the provided number. | Short | (no default value) | LOCAL | +| graph.use-hostname-for-unique-instance-id | When this is set, this JanusGraph's unique instance identifier is set to the hostname. If unique-instance-id-suffix is also set, then the identifier is set to . | Boolean | false | LOCAL | + +### gremlin +Gremlin configuration options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| gremlin.graph | The implementation of graph factory that will be used by gremlin server | String | org.janusgraph.core.JanusGraphFactory | LOCAL | + +### ids +General configuration options for graph element IDs + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| ids.block-size | Globally reserve graph element IDs in chunks of this size. Setting this too low will make commits frequently block on slow reservation requests. Setting it too high will result in IDs wasted when a graph instance shuts down with reserved but mostly-unused blocks. | Integer | 10000 | GLOBAL_OFFLINE | +| ids.flush | When true, vertices and edges are assigned IDs immediately upon creation. When false, IDs are assigned only when the transaction commits. Must be disabled for graph partitioning to work. | Boolean | true | MASKABLE | +| ids.num-partitions | Number of partition block to allocate for placement of vertices | Integer | 10 | MASKABLE | +| ids.placement | Name of the vertex placement strategy or full class name | String | simple | MASKABLE | +| ids.renew-percentage | When the most-recently-reserved ID block has only this percentage of its total IDs remaining (expressed as a value between 0 and 1), JanusGraph asynchronously begins reserving another block. This helps avoid transaction commits waiting on ID reservation even if the block size is relatively small. | Double | 0.3 | MASKABLE | +| ids.renew-timeout | The number of milliseconds that the JanusGraph id pool manager will wait before giving up on allocating a new block of ids | Duration | 120000 ms | MASKABLE | +| ids.store-name | The name of the ID KCVStore. IDS_STORE_NAME is meant to be used only for backward compatibility with Titan, and should not be used explicitly in normal operations or in new graphs. | String | janusgraph_ids | GLOBAL_OFFLINE | + +### ids.authority +Configuration options for graph element ID reservation/allocation + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| ids.authority.conflict-avoidance-mode | This setting helps separate JanusGraph instances sharing a single graph storage backend avoid contention when reserving ID blocks, increasing overall throughput. | ConflictAvoidanceMode | NONE | GLOBAL_OFFLINE | +| ids.authority.conflict-avoidance-tag | Conflict avoidance tag to be used by this JanusGraph instance when allocating IDs | Integer | 0 | LOCAL | +| ids.authority.conflict-avoidance-tag-bits | Configures the number of bits of JanusGraph-assigned element IDs that are reserved for the conflict avoidance tag | Integer | 4 | FIXED | +| ids.authority.randomized-conflict-avoidance-retries | Number of times the system attempts ID block reservations with random conflict avoidance tags before giving up and throwing an exception | Integer | 5 | MASKABLE | +| ids.authority.wait-time | The number of milliseconds the system waits for an ID block reservation to be acknowledged by the storage backend | Duration | 300 ms | GLOBAL_OFFLINE | + +### index * +Configuration options for the individual indexing backends + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| index.[X].backend | The indexing backend used to extend and optimize JanusGraph's query functionality. This setting is optional. JanusGraph can use multiple heterogeneous index backends. Hence, this option can appear more than once, so long as the user-defined name between "index" and "backend" is unique among appearances.Similar to the storage backend, this should be set to one of JanusGraph's built-in shorthand names for its standard index backends (shorthands: lucene, elasticsearch, es, solr) or to the full package and classname of a custom/third-party IndexProvider implementation. | String | elasticsearch | GLOBAL_OFFLINE | +| index.[X].conf-file | Path to a configuration file for those indexing backends that require/support a separate config file | String | (no default value) | MASKABLE | +| index.[X].directory | Directory to store index data locally | String | (no default value) | MASKABLE | +| index.[X].hostname | The hostname or comma-separated list of hostnames of index backend servers. This is only applicable to some index backends, such as elasticsearch and solr. | String[] | 127.0.0.1 | MASKABLE | +| index.[X].index-name | Name of the index if required by the indexing backend | String | janusgraph | GLOBAL_OFFLINE | +| index.[X].map-name | Whether to use the name of the property key as the field name in the index. It must be ensured, that theindexed property key names are valid field names. Renaming the property key will NOT rename the field and its the developers responsibility to avoid field collisions. | Boolean | true | GLOBAL | +| index.[X].max-result-set-size | Maxium number of results to return if no limit is specified. For index backends that support scrolling, it represents the number of results in each batch | Integer | 50 | MASKABLE | +| index.[X].port | The port on which to connect to index backend servers | Integer | (no default value) | MASKABLE | + +### index.[X].elasticsearch +Elasticsearch index configuration + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| index.[X].elasticsearch.bulk-refresh | Elasticsearch bulk API refresh setting used to control when changes made by this request are made visible to search | String | false | MASKABLE | +| index.[X].elasticsearch.health-request-timeout | When JanusGraph initializes its ES backend, JanusGraph waits up to this duration for the ES cluster health to reach at least yellow status. This string should be formatted as a natural number followed by the lowercase letter "s", e.g. 3s or 60s. | String | 30s | MASKABLE | +| index.[X].elasticsearch.interface | Interface for connecting to Elasticsearch. TRANSPORT_CLIENT and NODE were previously supported, but now are required to migrate to REST_CLIENT. See the JanusGraph upgrade instructions for more details. | String | REST_CLIENT | MASKABLE | +| index.[X].elasticsearch.max-retry-timeout | Sets the maximum timeout (in milliseconds) to honour in case of multiple retries of the same request sent using the ElasticSearch Rest Client by JanusGraph. | Integer | (no default value) | MASKABLE | +| index.[X].elasticsearch.scroll-keep-alive | How long (in secondes) elasticsearch should keep alive the scroll context. | Integer | 60 | GLOBAL_OFFLINE | +| index.[X].elasticsearch.use-all-field | Whether JanusGraph should add an "all" field mapping. When enabled field mappings will include a "copy_to" parameter referencing the "all" field. This is supported since Elasticsearch 6.x and is required when using wildcard fields starting in Elasticsearch 6.x. | Boolean | true | GLOBAL_OFFLINE | +| index.[X].elasticsearch.use-deprecated-multitype-index | Whether JanusGraph should group these indices into a single Elasticsearch index (requires Elasticsearch 5.x or earlier). | Boolean | false | GLOBAL_OFFLINE | + +### index.[X].elasticsearch.create +Settings related to index creation + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| index.[X].elasticsearch.create.allow-mapping-update | Whether JanusGraph should allow a mapping update when registering an index. Only applicable when use-external-mappings is true. | Boolean | false | MASKABLE | +| index.[X].elasticsearch.create.sleep | How long to sleep, in milliseconds, between the successful completion of a (blocking) index creation request and the first use of that index. This only applies when creating an index in ES, which typically only happens the first time JanusGraph is started on top of ES. If the index JanusGraph is configured to use already exists, then this setting has no effect. | Long | 200 | MASKABLE | +| index.[X].elasticsearch.create.use-external-mappings | Whether JanusGraph should make use of an external mapping when registering an index. | Boolean | false | MASKABLE | + +### index.[X].solr +Solr index configuration + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| index.[X].solr.configset | If specified, the same solr configSet can be resued for each new Collection that is created in SolrCloud. | String | (no default value) | MASKABLE | +| index.[X].solr.dyn-fields | Whether to use dynamic fields (which appends the data type to the field name). If dynamic fields is disabledthe user must map field names and define them explicitly in the schema. | Boolean | true | GLOBAL_OFFLINE | +| index.[X].solr.http-compression | Enable/disable compression on the HTTP connections made to Solr. | Boolean | false | MASKABLE | +| index.[X].solr.http-connection-timeout | Solr HTTP connection timeout. | Integer | 5000 | MASKABLE | +| index.[X].solr.http-max | Maximum number of HTTP connections in total to all Solr servers. | Integer | 100 | MASKABLE | +| index.[X].solr.http-max-per-host | Maximum number of HTTP connections per Solr host. | Integer | 20 | MASKABLE | +| index.[X].solr.http-urls | List of URLs to use to connect to Solr Servers (LBHttpSolrClient is used), don't add core or collection name to the URL. | String[] | http://localhost:8983/solr | MASKABLE | +| index.[X].solr.key-field-names | Field name that uniquely identifies each document in Solr. Must be specified as a list of `collection=field`. | String[] | (no default value) | GLOBAL | +| index.[X].solr.max-shards-per-node | Maximum number of shards per node. This applies when creating a new collection which is only supported under the SolrCloud operation mode. | Integer | 1 | GLOBAL_OFFLINE | +| index.[X].solr.mode | The operation mode for Solr which is either via HTTP (`http`) or using SolrCloud (`cloud`) | String | cloud | GLOBAL_OFFLINE | +| index.[X].solr.num-shards | Number of shards for a collection. This applies when creating a new collection which is only supported under the SolrCloud operation mode. | Integer | 1 | GLOBAL_OFFLINE | +| index.[X].solr.replication-factor | Replication factor for a collection. This applies when creating a new collection which is only supported under the SolrCloud operation mode. | Integer | 1 | GLOBAL_OFFLINE | +| index.[X].solr.ttl_field | Name of the TTL field for Solr collections. | String | ttl | GLOBAL_OFFLINE | +| index.[X].solr.wait-searcher | When mutating - wait for the index to reflect new mutations before returning. This can have a negative impact on performance. | Boolean | false | LOCAL | +| index.[X].solr.zookeeper-url | URL of the Zookeeper instance coordinating the SolrCloud cluster | String[] | localhost:2181 | MASKABLE | + +### log * +Configuration options for JanusGraph's logging system + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| log.[X].backend | Define the log backed to use | String | default | GLOBAL_OFFLINE | +| log.[X].fixed-partition | Whether all log entries are written to one fixed partition even if the backend store is partitioned.This can cause imbalanced loads and should only be used on low volume logs | Boolean | false | GLOBAL_OFFLINE | +| log.[X].key-consistent | Whether to require consistency for log reading and writing messages to the storage backend | Boolean | false | MASKABLE | +| log.[X].max-partitions | The maximum number of partitions to use for logging. Setting up this many actual or virtual partitions. Must be bigger than 0and a power of 2. | Integer | (no default value) | FIXED | +| log.[X].max-read-time | Maximum time in ms to try reading log messages from the backend before failing. | Duration | 4000 ms | MASKABLE | +| log.[X].max-write-time | Maximum time in ms to try persisting log messages against the backend before failing. | Duration | 10000 ms | MASKABLE | +| log.[X].num-buckets | The number of buckets to split log entries into for load balancing | Integer | 1 | GLOBAL_OFFLINE | +| log.[X].read-batch-size | Maximum number of log messages to read at a time for logging implementations that read messages in batches | Integer | 1024 | MASKABLE | +| log.[X].read-interval | Time in ms between message readings from the backend for this logging implementations that read message in batch | Duration | 5000 ms | MASKABLE | +| log.[X].read-lag-time | Maximum time in ms that it may take for reads to appear in the backend. If a write does not becomevisible in the storage backend in this amount of time, a log reader might miss the message. | Duration | 500 ms | MASKABLE | +| log.[X].read-threads | Number of threads to be used in reading and processing log messages | Integer | 1 | MASKABLE | +| log.[X].send-batch-size | Maximum number of log messages to batch up for sending for logging implementations that support batch sending | Integer | 256 | MASKABLE | +| log.[X].send-delay | Maximum time in ms that messages can be buffered locally before sending in batch | Duration | 1000 ms | MASKABLE | +| log.[X].ttl | Sets a TTL on all log entries, meaningthat all entries added to this log expire after the configured amount of time. Requiresthat the log implementation supports TTL. | Duration | (no default value) | GLOBAL | + +### metrics +Configuration options for metrics reporting + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.enabled | Whether to enable basic timing and operation count monitoring on backend | Boolean | false | MASKABLE | +| metrics.merge-stores | Whether to aggregate measurements for the edge store, vertex index, edge index, and ID store | Boolean | true | MASKABLE | +| metrics.prefix | The default name prefix for Metrics reported by JanusGraph. | String | org.janusgraph | MASKABLE | + +### metrics.console +Configuration options for metrics reporting to console + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.console.interval | Time between Metrics reports printing to the console, in milliseconds | Duration | (no default value) | MASKABLE | + +### metrics.csv +Configuration options for metrics reporting to CSV file + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.csv.directory | Metrics CSV output directory | String | (no default value) | MASKABLE | +| metrics.csv.interval | Time between dumps of CSV files containing Metrics data, in milliseconds | Duration | (no default value) | MASKABLE | + +### metrics.ganglia +Configuration options for metrics reporting through Ganglia + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.ganglia.addressing-mode | Whether to communicate to Ganglia via uni- or multicast | String | unicast | MASKABLE | +| metrics.ganglia.hostname | The unicast host or multicast group name to which Metrics will send Ganglia data | String | (no default value) | MASKABLE | +| metrics.ganglia.interval | The number of milliseconds to wait between sending Metrics data to Ganglia | Duration | (no default value) | MASKABLE | +| metrics.ganglia.port | The port to which Ganglia data are sent | Integer | 8649 | MASKABLE | +| metrics.ganglia.protocol-31 | Whether to send data to Ganglia in the 3.1 protocol format | Boolean | true | MASKABLE | +| metrics.ganglia.spoof | If non-null, it must be a valid Gmetric spoof string formatted as an IP:hostname pair. See https://github.com/ganglia/monitor-core/wiki/Gmetric-Spoofing for information about this setting. | String | (no default value) | MASKABLE | +| metrics.ganglia.ttl | The multicast TTL to set on outgoing Ganglia datagrams | Integer | 1 | MASKABLE | +| metrics.ganglia.uuid | The host UUID to set on outgoing Ganglia datagrams. See https://github.com/ganglia/monitor-core/wiki/UUIDSources for information about this setting. | String | (no default value) | LOCAL | + +### metrics.graphite +Configuration options for metrics reporting through Graphite + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.graphite.hostname | The hostname to receive Graphite plaintext protocol metric data | String | (no default value) | MASKABLE | +| metrics.graphite.interval | The number of milliseconds to wait between sending Metrics data | Duration | (no default value) | MASKABLE | +| metrics.graphite.port | The port to which Graphite data are sent | Integer | 2003 | MASKABLE | +| metrics.graphite.prefix | A Graphite-specific prefix for reported metrics | String | (no default value) | MASKABLE | + +### metrics.jmx +Configuration options for metrics reporting through JMX + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.jmx.agentid | The JMX agentId used by Metrics | String | (no default value) | MASKABLE | +| metrics.jmx.domain | The JMX domain in which to report Metrics | String | (no default value) | MASKABLE | +| metrics.jmx.enabled | Whether to report Metrics through a JMX MBean | Boolean | false | MASKABLE | + +### metrics.slf4j +Configuration options for metrics reporting through slf4j + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| metrics.slf4j.interval | Time between slf4j logging reports of Metrics data, in milliseconds | Duration | (no default value) | MASKABLE | +| metrics.slf4j.logger | The complete name of the Logger through which Metrics will report via Slf4j | String | (no default value) | MASKABLE | + +### query +Configuration options for query processing + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| query.batch | Whether traversal queries should be batched when executed against the storage backend. This can lead to significant performance improvement if there is a non-trivial latency to the backend. | Boolean | false | MASKABLE | +| query.fast-property | Whether to pre-fetch all properties on first singular vertex property access. This can eliminate backend calls on subsequentproperty access for the same vertex at the expense of retrieving all properties at once. This can be expensive for vertices with many properties | Boolean | true | MASKABLE | +| query.force-index | Whether JanusGraph should throw an exception if a graph query cannot be answered using an index. Doing solimits the functionality of JanusGraph's graph queries but ensures that slow graph queries are avoided on large graphs. Recommended for production use of JanusGraph. | Boolean | false | MASKABLE | +| query.ignore-unknown-index-key | Whether to ignore undefined types encountered in user-provided index queries | Boolean | false | MASKABLE | +| query.smart-limit | Whether the query optimizer should try to guess a smart limit for the query to ensure responsiveness in light of possibly large result sets. Those will be loaded incrementally if this option is enabled. | Boolean | true | MASKABLE | + +### schema +Schema related configuration options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| schema.default | Configures the DefaultSchemaMaker to be used by this graph. If set to 'none', automatic schema creation is disabled. Defaults to a blueprints compatible schema maker with MULTI edge labels and SINGLE property keys | String | default | MASKABLE | + +### storage +Configuration options for the storage backend. Some options are applicable only for certain backends. + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.backend | The primary persistence provider used by JanusGraph. This is required. It should be set one of JanusGraph's built-in shorthand names for its standard storage backends (shorthands: berkeleyje, cassandrathrift, cassandra, astyanax, embeddedcassandra, cql, hbase, inmemory) or to the full package and classname of a custom/third-party StoreManager implementation. | String | (no default value) | LOCAL | +| storage.batch-loading | Whether to enable batch loading into the storage backend | Boolean | false | LOCAL | +| storage.buffer-size | Size of the batch in which mutations are persisted | Integer | 1024 | MASKABLE | +| storage.conf-file | Path to a configuration file for those storage backends which require/support a single separate config file. | String | (no default value) | LOCAL | +| storage.connection-timeout | Default timeout, in milliseconds, when connecting to a remote database instance | Duration | 10000 ms | MASKABLE | +| storage.directory | Storage directory for those storage backends that require local storage. | String | (no default value) | LOCAL | +| storage.drop-on-clear | Whether to drop the graph database (true) or delete rows (false) when clearing storage. Note that some backends always drop the graph database when clearing storage. Also note that indices are always dropped when clearing storage. | Boolean | true | MASKABLE | +| storage.hostname | The hostname or comma-separated list of hostnames of storage backend servers. This is only applicable to some storage backends, such as cassandra and hbase. | String[] | 127.0.0.1 | LOCAL | +| storage.page-size | JanusGraph break requests that may return many results from distributed storage backends into a series of requests for small chunks/pages of results, where each chunk contains up to this many elements. | Integer | 100 | MASKABLE | +| storage.parallel-backend-ops | Whether JanusGraph should attempt to parallelize storage operations | Boolean | true | MASKABLE | +| storage.password | Password to authenticate against backend | String | (no default value) | LOCAL | +| storage.port | The port on which to connect to storage backend servers. For HBase, it is the Zookeeper port. | Integer | (no default value) | LOCAL | +| storage.read-only | Read-only database | Boolean | false | LOCAL | +| storage.read-time | Maximum time (in ms) to wait for a backend read operation to complete successfully. If a backend read operationfails temporarily, JanusGraph will backoff exponentially and retry the operation until the wait time has been exhausted. | Duration | 10000 ms | MASKABLE | +| storage.root | Storage root directory for those storage backends that require local storage. If you do not supply storage.directory and you do supply graph.graphname, then your data will be stored in the directory equivalent to /. | String | (no default value) | LOCAL | +| storage.setup-wait | Time in milliseconds for backend manager to wait for the storage backends to become available when JanusGraph is run in server mode | Duration | 60000 ms | MASKABLE | +| storage.transactions | Enables transactions on storage backends that support them | Boolean | true | MASKABLE | +| storage.username | Username to authenticate against backend | String | (no default value) | LOCAL | +| storage.write-time | Maximum time (in ms) to wait for a backend write operation to complete successfully. If a backend write operationfails temporarily, JanusGraph will backoff exponentially and retry the operation until the wait time has been exhausted. | Duration | 100000 ms | MASKABLE | + +### storage.berkeleyje +BerkeleyDB JE configuration options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.berkeleyje.cache-percentage | Percentage of JVM heap reserved for BerkeleyJE's cache | Integer | 65 | MASKABLE | +| storage.berkeleyje.isolation-level | The isolation level used by transactions | String | REPEATABLE_READ | MASKABLE | +| storage.berkeleyje.lock-mode | The BDB record lock mode used for read operations | String | LockMode.DEFAULT | MASKABLE | + +### storage.cassandra +Cassandra storage backend options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cassandra.atomic-batch-mutate | True to use Cassandra atomic batch mutation, false to use non-atomic batches | Boolean | true | MASKABLE | +| storage.cassandra.compaction-strategy-class | The compaction strategy to use for JanusGraph tables | String | (no default value) | FIXED | +| storage.cassandra.compaction-strategy-options | Compaction strategy options. This list is interpreted as a map. It must have an even number of elements in [key,val,key,val,...] form. | String[] | (no default value) | FIXED | +| storage.cassandra.compression | Whether the storage backend should use compression when storing the data | Boolean | true | FIXED | +| storage.cassandra.compression-block-size | The size of the compression blocks in kilobytes | Integer | 64 | FIXED | +| storage.cassandra.compression-type | The sstable_compression value JanusGraph uses when creating column families. This accepts any value allowed by Cassandra's sstable_compression option. Leave this unset to disable sstable_compression on JanusGraph-created CFs. | String | LZ4Compressor | MASKABLE | +| storage.cassandra.frame-size-mb | The thrift frame size in megabytes | Integer | 15 | MASKABLE | +| storage.cassandra.keyspace | The name of JanusGraph's keyspace. It will be created if it does not exist. If it is not supplied, but graph.graphname is, then the the keyspace will be set to that. | String | janusgraph | LOCAL | +| storage.cassandra.read-consistency-level | The consistency level of read operations against Cassandra | String | QUORUM | MASKABLE | +| storage.cassandra.replication-factor | The number of data replicas (including the original copy) that should be kept. This is only meaningful for storage backends that natively support data replication. | Integer | 1 | GLOBAL_OFFLINE | +| storage.cassandra.replication-strategy-class | The replication strategy to use for JanusGraph keyspace | String | org.apache.cassandra.locator.SimpleStrategy | FIXED | +| storage.cassandra.replication-strategy-options | Replication strategy options, e.g. factor or replicas per datacenter. This list is interpreted as a map. It must have an even number of elements in [key,val,key,val,...] form. A replication_factor set here takes precedence over one set with storage.cassandra.replication-factor | String[] | (no default value) | FIXED | +| storage.cassandra.write-consistency-level | The consistency level of write operations against Cassandra | String | QUORUM | MASKABLE | + +### storage.cassandra.astyanax +Astyanax-specific Cassandra options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cassandra.astyanax.cluster-name | Default name for the Cassandra cluster | String | JanusGraph Cluster | MASKABLE | +| storage.cassandra.astyanax.connection-pool-type | Astyanax's connection pooler implementation | String | TOKEN_AWARE | MASKABLE | +| storage.cassandra.astyanax.frame-size | The thrift frame size in mega bytes | Integer | 15 | MASKABLE | +| storage.cassandra.astyanax.host-supplier | Host supplier to use when discovery type is set to DISCOVERY_SERVICE or TOKEN_AWARE | String | (no default value) | MASKABLE | +| storage.cassandra.astyanax.local-datacenter | The name of the local or closest Cassandra datacenter. When set and not whitespace, this value will be passed into ConnectionPoolConfigurationImpl.setLocalDatacenter. When unset or set to whitespace, setLocalDatacenter will not be invoked. | String | (no default value) | MASKABLE | +| storage.cassandra.astyanax.max-cluster-connections-per-host | Maximum pooled "cluster" connections per host | Integer | 3 | MASKABLE | +| storage.cassandra.astyanax.max-connections | Maximum open connections allowed in the pool (counting all hosts) | Integer | -1 | MASKABLE | +| storage.cassandra.astyanax.max-connections-per-host | Maximum pooled connections per host | Integer | 32 | MASKABLE | +| storage.cassandra.astyanax.max-operations-per-connection | Maximum number of operations allowed per connection before the connection is closed | Integer | 100000 | MASKABLE | +| storage.cassandra.astyanax.node-discovery-type | How Astyanax discovers Cassandra cluster nodes | String | RING_DESCRIBE | MASKABLE | +| storage.cassandra.astyanax.read-page-size | The page size for Cassandra read operations | Integer | 4096 | MASKABLE | +| storage.cassandra.astyanax.retry-backoff-strategy | Astyanax's retry backoff strategy with configuration parameters | String | com.netflix.astyanax.connectionpool.impl.FixedRetryBackoffStrategy,1000,5000 | MASKABLE | +| storage.cassandra.astyanax.retry-delay-slice | Astyanax's connection pool "retryDelaySlice" parameter | Integer | 10000 | MASKABLE | +| storage.cassandra.astyanax.retry-max-delay-slice | Astyanax's connection pool "retryMaxDelaySlice" parameter | Integer | 10 | MASKABLE | +| storage.cassandra.astyanax.retry-policy | Astyanax's retry policy implementation with configuration parameters | String | com.netflix.astyanax.retry.BoundedExponentialBackoff,100,25000,8 | MASKABLE | +| storage.cassandra.astyanax.retry-suspend-window | Astyanax's connection pool "retryMaxDelaySlice" parameter | Integer | 20000 | MASKABLE | + +### storage.cassandra.ssl +Configuration options for SSL + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cassandra.ssl.enabled | Controls use of the SSL connection to Cassandra | Boolean | false | LOCAL | + +### storage.cassandra.ssl.truststore +Configuration options for SSL Truststore. + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cassandra.ssl.truststore.location | Marks the location of the SSL Truststore. | String | | LOCAL | +| storage.cassandra.ssl.truststore.password | The password to access SSL Truststore. | String | | LOCAL | + +### storage.cassandra.thrift.cpool +Options for the Apache commons-pool connection manager + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cassandra.thrift.cpool.evictor-period | Approximate number of milliseconds between runs of the idle connection evictor. Set to -1 to never run the idle connection evictor. | Long | 30000 | MASKABLE | +| storage.cassandra.thrift.cpool.idle-test | Whether the idle connection evictor validates idle connections and drops those that fail to validate | Boolean | false | MASKABLE | +| storage.cassandra.thrift.cpool.idle-tests-per-eviction-run | When the value is negative, e.g. -n, roughly one nth of the idle connections are tested per run. When the value is positive, e.g. n, the min(idle-count, n) connections are tested per run. | Integer | 0 | MASKABLE | +| storage.cassandra.thrift.cpool.max-active | Maximum number of concurrently in-use connections (-1 to leave undefined) | Integer | 16 | MASKABLE | +| storage.cassandra.thrift.cpool.max-idle | Maximum number of concurrently idle connections (-1 to leave undefined) | Integer | 4 | MASKABLE | +| storage.cassandra.thrift.cpool.max-total | Max number of allowed Thrift connections, idle or active (-1 to leave undefined) | Integer | -1 | MASKABLE | +| storage.cassandra.thrift.cpool.max-wait | Maximum number of milliseconds to block when storage.cassandra.thrift.cpool.when-exhausted is set to BLOCK. Has no effect when set to actions besides BLOCK. Set to -1 to wait indefinitely. | Long | -1 | MASKABLE | +| storage.cassandra.thrift.cpool.min-evictable-idle-time | Minimum number of milliseconds a connection must be idle before it is eligible for eviction. See also storage.cassandra.thrift.cpool.evictor-period. Set to -1 to never evict idle connections. | Long | 60000 | MASKABLE | +| storage.cassandra.thrift.cpool.min-idle | Minimum number of idle connections the pool attempts to maintain | Integer | 0 | MASKABLE | +| storage.cassandra.thrift.cpool.when-exhausted | What to do when clients concurrently request more active connections than are allowed by the pool. The value must be one of BLOCK, FAIL, or GROW. | String | BLOCK | MASKABLE | + +### storage.cql +CQL storage backend options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cql.atomic-batch-mutate | True to use Cassandra atomic batch mutation, false to use non-atomic batches | Boolean | false | MASKABLE | +| storage.cql.batch-statement-size | The number of statements in each batch | Integer | 20 | MASKABLE | +| storage.cql.cluster-name | Default name for the Cassandra cluster | String | JanusGraph Cluster | MASKABLE | +| storage.cql.compact-storage | Whether the storage backend should use compact storage on tables. This option is only available for Cassandra 2 and earlier and defaults to true. | Boolean | true | FIXED | +| storage.cql.compaction-strategy-class | The compaction strategy to use for JanusGraph tables | String | (no default value) | FIXED | +| storage.cql.compaction-strategy-options | Compaction strategy options. This list is interpreted as a map. It must have an even number of elements in [key,val,key,val,...] form. | String[] | (no default value) | FIXED | +| storage.cql.compression | Whether the storage backend should use compression when storing the data | Boolean | true | FIXED | +| storage.cql.compression-block-size | The size of the compression blocks in kilobytes | Integer | 64 | FIXED | +| storage.cql.compression-type | The sstable_compression value JanusGraph uses when creating column families. This accepts any value allowed by Cassandra's sstable_compression option. Leave this unset to disable sstable_compression on JanusGraph-created CFs. | String | LZ4Compressor | MASKABLE | +| storage.cql.keyspace | The name of JanusGraph's keyspace. It will be created if it does not exist. | String | janusgraph | LOCAL | +| storage.cql.local-datacenter | The name of the local or closest Cassandra datacenter. When set and not whitespace, this value will be passed into ConnectionPoolConfigurationImpl.setLocalDatacenter. When unset or set to whitespace, setLocalDatacenter will not be invoked. | String | (no default value) | MASKABLE | +| storage.cql.only-use-local-consistency-for-system-operations | True to prevent any system queries from using QUORUM consistency and always use LOCAL_QUORUM instead | Boolean | false | MASKABLE | +| storage.cql.protocol-version | The protocol version used to connect to the Cassandra database. If no value is supplied then the driver will negotiate with the server. | Integer | 0 | LOCAL | +| storage.cql.read-consistency-level | The consistency level of read operations against Cassandra | String | QUORUM | MASKABLE | +| storage.cql.replication-factor | The number of data replicas (including the original copy) that should be kept | Integer | 1 | GLOBAL_OFFLINE | +| storage.cql.replication-strategy-class | The replication strategy to use for JanusGraph keyspace | String | SimpleStrategy | FIXED | +| storage.cql.replication-strategy-options | Replication strategy options, e.g. factor or replicas per datacenter. This list is interpreted as a map. It must have an even number of elements in [key,val,key,val,...] form. A replication_factor set here takes precedence over one set with storage.cql.replication-factor | String[] | (no default value) | FIXED | +| storage.cql.write-consistency-level | The consistency level of write operations against Cassandra | String | QUORUM | MASKABLE | + +### storage.cql.ssl +Configuration options for SSL + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cql.ssl.enabled | Controls use of the SSL connection to Cassandra | Boolean | false | LOCAL | + +### storage.cql.ssl.truststore +Configuration options for SSL Truststore. + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.cql.ssl.truststore.location | Marks the location of the SSL Truststore. | String | | LOCAL | +| storage.cql.ssl.truststore.password | The password to access SSL Truststore. | String | | LOCAL | + +### storage.hbase +HBase storage options + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.hbase.compat-class | The package and class name of the HBaseCompat implementation. HBaseCompat masks version-specific HBase API differences. When this option is unset, JanusGraph calls HBase's VersionInfo.getVersion() and loads the matching compat class at runtime. Setting this option forces JanusGraph to instead reflectively load and instantiate the specified class. | String | (no default value) | MASKABLE | +| storage.hbase.compression-algorithm | An HBase Compression.Algorithm enum string which will be applied to newly created column families. The compression algorithm must be installed and available on the HBase cluster. JanusGraph cannot install and configure new compression algorithms on the HBase cluster by itself. | String | GZ | MASKABLE | +| storage.hbase.region-count | The number of initial regions set when creating JanusGraph's HBase table | Integer | (no default value) | MASKABLE | +| storage.hbase.regions-per-server | The number of regions per regionserver to set when creating JanusGraph's HBase table | Integer | (no default value) | MASKABLE | +| storage.hbase.short-cf-names | Whether to shorten the names of JanusGraph's column families to one-character mnemonics to conserve storage space | Boolean | true | FIXED | +| storage.hbase.skip-schema-check | Assume that JanusGraph's HBase table and column families already exist. When this is true, JanusGraph will not check for the existence of its table/CFs, nor will it attempt to create them under any circumstances. This is useful when running JanusGraph without HBase admin privileges. | Boolean | false | MASKABLE | +| storage.hbase.table | The name of the table JanusGraph will use. When storage.hbase.skip-schema-check is false, JanusGraph will automatically create this table if it does not already exist. If this configuration option is not provided but graph.graphname is, the table will be set to that value. | String | janusgraph | LOCAL | + +### storage.lock +Options for locking on eventually-consistent stores + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.lock.backend | Locker type to use | String | consistentkey | GLOBAL_OFFLINE | +| storage.lock.clean-expired | Whether to delete expired locks from the storage backend | Boolean | false | MASKABLE | +| storage.lock.expiry-time | Number of milliseconds after which a lock is considered to have expired. Lock applications that were not released are considered expired after this time and released. This value should be larger than the maximum time a transaction can take in order to guarantee that no correctly held applications are expired pre-maturely and as small as possible to avoid dead lock. | Duration | 300000 ms | GLOBAL_OFFLINE | +| storage.lock.local-mediator-group | This option determines the LocalLockMediator instance used for early detection of lock contention between concurrent JanusGraph graph instances within the same process which are connected to the same storage backend. JanusGraph instances that have the same value for this variable will attempt to discover lock contention among themselves in memory before proceeding with the general-case distributed locking code. JanusGraph generates an appropriate default value for this option at startup. Overridding the default is generally only useful in testing. | String | (no default value) | LOCAL | +| storage.lock.retries | Number of times the system attempts to acquire a lock before giving up and throwing an exception | Integer | 3 | MASKABLE | +| storage.lock.wait-time | Number of milliseconds the system waits for a lock application to be acknowledged by the storage backend. Also, the time waited at the end of all lock applications before verifying that the applications were successful. This value should be a small multiple of the average consistent write time. | Duration | 100 ms | GLOBAL_OFFLINE | + +### storage.meta * +Meta data to include in storage backend retrievals + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| storage.meta.[X].timestamps | Whether to include timestamps in retrieved entries for storage backends that automatically annotated entries with timestamps | Boolean | false | GLOBAL | +| storage.meta.[X].ttl | Whether to include ttl in retrieved entries for storage backends that support storage and retrieval of cell level TTL | Boolean | false | GLOBAL | +| storage.meta.[X].visibility | Whether to include visibility in retrieved entries for storage backends that support cell level visibility | Boolean | true | GLOBAL | + +### tx +Configuration options for transaction handling + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| tx.log-tx | Whether transaction mutations should be logged to JanusGraph's write-ahead transaction log which can be used for recovery of partially failed transactions | Boolean | false | GLOBAL | +| tx.max-commit-time | Maximum time (in ms) that a transaction might take to commit against all backends. This is used by the distributed write-ahead log processing to determine when a transaction can be considered failed (i.e. after this time has elapsed).Must be longer than the maximum allowed write time. | Duration | 10000 ms | GLOBAL | + +### tx.recovery +Configuration options for transaction recovery processes + + +| Name | Description | Datatype | Default Value | Mutability | +| ---- | ---- | ---- | ---- | ---- | +| tx.recovery.verbose | Whether the transaction recovery system should print recovered transactions and other activity to standard output | Boolean | false | MASKABLE | diff --git a/docs/static/images/minimalist-scenario.svg b/docs/basics/minimalist-scenario.svg similarity index 100% rename from docs/static/images/minimalist-scenario.svg rename to docs/basics/minimalist-scenario.svg diff --git a/docs/static/images/minimalist-scenario.xml b/docs/basics/minimalist-scenario.xml similarity index 100% rename from docs/static/images/minimalist-scenario.xml rename to docs/basics/minimalist-scenario.xml diff --git a/docs/basics/multi-node.md b/docs/basics/multi-node.md new file mode 100644 index 0000000000..fdcf41969b --- /dev/null +++ b/docs/basics/multi-node.md @@ -0,0 +1,170 @@ +Things to Consider in a Multi-Node JanusGraph Cluster +===================================================== + +JanusGraph is a distributed graph database, which means it can be setup +in a multi-node cluster. However, when working in such an environment, +there are important things to consider. Furthermore, if configured +properly, JanusGraph handles some of these special considerations for +the user. + +Dynamic Graphs +-------------- + +JanusGraph supports [dynamically creating +graphs](configured-graph-factory.md#configuredgraphfactory). This is +deviation from the way in which standard Gremlin Server implementations +allow one to access a graph. Traditionally, users create bindings to +graphs at server-start, by configuring the gremlin-server.yaml file +accordingly. For example, if the `graphs` section of your yaml file +looks like this: +```yaml +graphs { + graph1: conf/graph1.properties, + graph2: conf/graph2.properties +} +``` + +then you will access your graphs on the Gremlin Server using the fact +that the String `graph1` will be bound to the graph opened on the server +as per its supplied properties file, and the same holds true for +`graph2`. + +However, if we use the `ConfiguredGraphFactory` to dynamically create +graphs, then those graphs are managed by the +[JanusGraphManager](configured-graph-factory.md#janusgraphmanager) and +the graph configurations are managed by the +[ConfigurationManagementGraph](configured-graph-factory.md#configurationmanagementgraph). +This is especially useful because it 1. allows you to define graph +configurations post-server-start and 2. allows the graph configurations +to be managed in a persisted and distributed nature across your +JanusGraph cluster. + +To properly use the `ConfiguredGraphFactory`, you must configure every +Gremlin Server in your cluster to use the `JanusGraphManager` and the +`ConfigurationManagementGraph`. This procedure is explained in detail +[here](configured-graph-factory.md#configuring-janusgraph-server-for-configuredgraphfactory). + +### Graph Reference Consistency + +If you configure all your JanusGraph servers to use the +[ConfiguredGraphFactory](configured-graph-factory.md#configuring-janusgraph-server-for-configuredgraphfactory), +JanusGraph will ensure all graph representations are-up-to-date across +all JanusGraph nodes in your cluster. + +For example, if you update or delete the configuration to a graph on one +JanusGraph node, then we must evict that graph from the cache on *every +JanusGraph node in the cluster*. Otherwise, we may have inconsistent +graph representations across your cluster. JanusGraph automatically +handles this eviction using a messaging log queue through the backend +system that the graph in question is configured to use. + +If one of your servers is configured incorrectly, then it may not be +able to successfully remove the graph from the cache. + +!!! important + Any updates to your + [TemplateConfiguration](configured-graph-factory.md#template-configuration) + will not result in the updating of graphs/graph configurations + previously created using said template configuration. If you want to + update the individual graph configurations, you must do so using the + [available update + APIs](configured-graph-factory.md#updating-configurations). These + update APIs will *then* result in the graphe cache eviction across all + JanusGraph nodes in your cluster. + +### Dynamic Graph and Traversal Bindings + +JanusGraph has the ability to bind dynamically created graphs and their +traversal references to `` and +`_traversal`, respectively, across all JanusGraph nodes +in your cluster, with a maximum of a 20s lag for the binding to take +effect on any node in the cluster. Read more about this +[here](configured-graph-factory.md#graph-and-traversal-bindings). + +JanusGraph accomplishes this by having each node in your cluster poll +the `ConfigurationManagementGraph` for all graphs for which you have +created configurations. The `JanusGraphManager` will then open said +graph with its persisted configuration, store it in its graph cache, and +bind the `` to the graph reference on the +`GremlinExecutor` as well as bind `_traversal` to the +graph’s traversal reference on the `GremlinExecutor`. + +This allows you to access a dynamically created graph and its traversal +reference by their string bindings, on every node in your JanusGraph +cluster. This is particularly important to be able to work with Gremlin +Server clients and use [TinkerPops’s withRemote functionality](#using-tinkerpops-withremote-functionality). + +#### Set Up + +To set up your cluster to bind dynamically created graphs and their +traversal references, you must: + +1. Configure each node to use the + [ConfiguredGraphFactory](configured-graph-factory.md#configuring-JanusGraph-server-for-configuredgraphfactory). + +2. Configure each node to use a `JanusGraphChannelizer`, which injects + lower-level Gremlin Server components, like the GremlinExecutor, + into the JanusGraph project, giving us greater control of the + Gremlin Server. + +To configure each node to use a `JanusGraphChannelizer`, we must update +the `gremlin-server.yaml` to do so: + + channelizer: org.janusgraph.channelizers.JanusGraphWebSocketChannelizer + +There are a few channelizers you can choose from: + +1. org.janusgraph.channelizers.JanusGraphWebSocketChannelizer + +2. org.janusgraph.channelizers.JanusGraphHttpChannelizer + +3. org.janusgraph.channelizers.JanusGraphNioChannelizer + +4. org.janusgraph.channelizers.JanusGraphWsAndHttpChannelizer + +All of the channelizers share the exact same functionality as their +TinkerPop counterparts. + +#### Using TinkerPop’s withRemote Functionality + +Since traversal references are bound on the JanusGraph servers, we can +make use of [TinkerPop’s withRemote +functionality](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/#connecting-via-remotegraph). +This will allow one to run gremlin queries locally, against a remote +graph reference. Traditionally, one runs queries against remote Gremlin +Servers by sending String script representations, which are processed on +the remote server and the response serialized and sent back. However, +TinkerPop also allows for the use of `remoteGraph`, which could be +useful if you are building a TinkerPop compliant graph infrastructure +that is easily transferable to multiple implementations. + +To use this functionality in JanusGraph, we must first ensure we have +created a graph on the remote JanusGraph cluster: + +``` ConfiguredGraphFactory.create("graph1"); ``` + +Next, we must wait 20 seconds to ensure the traversal reference is bound +on every JanusGraph node in the remote cluster. + +Finally, we can locally make use of the `withRemote` method to access a +local reference to a remote graph: +```groovy +gremlin> cluster = Cluster.open('conf/remote-objects.yaml') +==>localhost/127.0.0.1:8182 +gremlin> graph = EmptyGraph.instance() +==>emptygraph[empty] +gremlin> g = graph.traversal().withRemote(DriverRemoteConnection.using(cluster, "graph1_traversal")) +==>graphtraversalsource[emptygraph[empty], standard] +``` + +For completion, the above `conf/remote-objects.yaml` should tell the +`Cluster` API how to access the remote JanusGraph servers; for example, +it may look like: +```yaml +hosts: [remoteaddress1.com, remoteaddress2.com] +port: 8182 +username: admin +password: password +connectionPool: { enableSsl: true } +serializer: { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }} +``` diff --git a/docs/basics/schema.md b/docs/basics/schema.md new file mode 100644 index 0000000000..5a75244103 --- /dev/null +++ b/docs/basics/schema.md @@ -0,0 +1,265 @@ +# Schema and Data Modeling + +Each JanusGraph graph has a schema comprised of the edge labels, +property keys, and vertex labels used therein. A JanusGraph schema can +either be explicitly or implicitly defined. Users are encouraged to +explicitly define the graph schema during application development. An +explicitly defined schema is an important component of a robust graph +application and greatly improves collaborative software development. +Note, that a JanusGraph schema can be evolved over time without any +interruption of normal database operations. Extending the schema does +not slow down query answering and does not require database downtime. + +The schema type - i.e. edge label, property key, or vertex label - is +assigned to elements in the graph - i.e. edge, properties or vertices +respectively - when they are first created. The assigned schema type +cannot be changed for a particular element. This ensures a stable type +system that is easy to reason about. + +Beyond the schema definition options explained in this section, schema +types provide performance tuning options that are discussed in +[Advanced Schema](../advanced-topics/advschema.md). + +## Displaying Schema Information + +There are methods to view specific elements of the graph schema within the management API. +These methods are `mgmt.printIndexes()`, `mgmt.printPropertyKeys()`, `mgmt.printVertexLabels()`, and `mgmt.printEdgeLabels()`. +There is also a method that displays all the combined output named `printSchema()`. + +```groovy +mgmt = graph.openManagement() +mgmt.printSchema() +``` + +## Defining Edge Labels + +Each edge connecting two vertices has a label which defines the +semantics of the relationship. For instance, an edge labeled `friend` +between vertices A and B encodes a friendship between the two +individuals. + +To define an edge label, call `makeEdgeLabel(String)` on an open graph +or management transaction and provide the name of the edge label as the +argument. Edge label names must be unique in the graph. This method +returns a builder for edge labels that allows to define its +multiplicity. The **multiplicity** of an edge label defines a +multiplicity constraint on all edges of this label, that is, a maximum +number of edges between pairs of vertices. JanusGraph recognizes the +following multiplicity settings. + +### Edge Label Multiplicity + +- **MULTI**: Allows multiple edges of the same label between any pair + of vertices. In other words, the graph is a *multi graph* with + respect to such edge label. There is no constraint on edge + multiplicity. +- **SIMPLE**: Allows at most one edge of such label between any pair + of vertices. In other words, the graph is a *simple graph* with + respect to the label. Ensures that edges are unique for a given + label and pairs of vertices. +- **MANY2ONE**: Allows at most one outgoing edge of such label on any + vertex in the graph but places no constraint on incoming edges. The + edge label `mother` is an example with MANY2ONE multiplicity since + each person has at most one mother but mothers can have multiple + children. +- **ONE2MANY**: Allows at most one incoming edge of such label on any + vertex in the graph but places no constraint on outgoing edges. The + edge label `winnerOf` is an example with ONE2MANY multiplicity since + each contest is won by at most one person but a person can win + multiple contests. +- **ONE2ONE**: Allows at most one incoming and one outgoing edge of + such label on any vertex in the graph. The edge label *marriedTo* is + an example with ONE2ONE multiplicity since a person is married to + exactly one other person. + +The default multiplicity is MULTI. The definition of an edge label is +completed by calling the `make()` method on the builder which returns +the defined edge label as shown in the following example. +```groovy +mgmt = graph.openManagement() +follow = mgmt.makeEdgeLabel('follow').multiplicity(MULTI).make() +mother = mgmt.makeEdgeLabel('mother').multiplicity(MANY2ONE).make() +mgmt.commit() +``` + +## Defining Property Keys + +Properties on vertices and edges are key-value pairs. For instance, the +property `name='Daniel'` has the key `name` and the value `'Daniel'`. +Property keys are part of the JanusGraph schema and can constrain the +allowed data types and cardinality of values. + +To define a property key, call `makePropertyKey(String)` on an open +graph or management transaction and provide the name of the property key +as the argument. Property key names must be unique in the graph, and it +is recommended to avoid spaces or special characters in property names. +This method returns a builder for the property keys. + +### Property Key Data Type + +Use `dataType(Class)` to define the data type of a property key. +JanusGraph will enforce that all values associated with the key have the +configured data type and thereby ensures that data added to the graph is +valid. For instance, one can define that the `name` key has a String +data type. + +Define the data type as `Object.class` in order to allow any +(serializable) value to be associated with a key. However, it is +encouraged to use concrete data types whenever possible. Configured data +types must be concrete classes and not interfaces or abstract classes. +JanusGraph enforces class equality, so adding a sub-class of a +configured data type is not allowed. + +JanusGraph natively supports the following data types. + +
Native JanusGraph Data Types
+ +| Name | Description | +| ---- | ----- | +| String | Character sequence | +| Character | Individual character | +| Boolean | true or false | +| Byte | byte value | +| Short | short value | +| Integer | integer value | +| Long | long value | +| Float | 4 byte floating point number | +| Double | 8 byte floating point number | +| Date | Specific instant in time (`java.util.Date`) | +| Geoshape | Geographic shape like point, circle or box | +| UUID | Universally unique identifier (`java.util.UUID`) | + +### Property Key Cardinality + +Use `cardinality(Cardinality)` to define the allowed cardinality of the +values associated with the key on any given vertex. + +- **SINGLE**: Allows at most one value per element for such key. In + other words, the key→value mapping is unique for all elements in the + graph. The property key `birthDate` is an example with SINGLE + cardinality since each person has exactly one birth date. +- **LIST**: Allows an arbitrary number of values per element for such + key. In other words, the key is associated with a list of values + allowing duplicate values. Assuming we model sensors as vertices in + a graph, the property key `sensorReading` is an example with LIST + cardinality to allow lots of (potentially duplicate) sensor readings + to be recorded. +- **SET**: Allows multiple values but no duplicate values per element + for such key. In other words, the key is associated with a set of + values. The property key `name` has SET cardinality if we want to + capture all names of an individual (including nick name, maiden + name, etc). + +The default cardinality setting is SINGLE. Note, that property keys used +on edges and properties have cardinality SINGLE. Attaching multiple +values for a single key on an edge or property is not supported. + +```java +mgmt = graph.openManagement() +birthDate = mgmt.makePropertyKey('birthDate').dataType(Long.class).cardinality(Cardinality.SINGLE).make() +name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make() +sensorReading = mgmt.makePropertyKey('sensorReading').dataType(Double.class).cardinality(Cardinality.LIST).make() +mgmt.commit() +``` + +## Relation Types + +Edge labels and property keys are jointly referred to as **relation +types**. Names of relation types must be unique in the graph which means +that property keys and edge labels cannot have the same name. There are +methods in the JanusGraph API to query for the existence or retrieve +relation types which encompasses both property keys and edge labels. + +```java +mgmt = graph.openManagement() +if (mgmt.containsRelationType('name')) + name = mgmt.getPropertyKey('name') +mgmt.getRelationTypes(EdgeLabel.class) +mgmt.commit() +``` + +## Defining Vertex Labels + +Like edges, vertices have labels. Unlike edge labels, vertex labels are +optional. Vertex labels are useful to distinguish different types of +vertices, e.g. *user* vertices and *product* vertices. + +Although labels are optional at the conceptual and data model level, +JanusGraph assigns all vertices a label as an internal implementation +detail. Vertices created by the `addVertex` methods use JanusGraph’s +default label. + +To create a label, call `makeVertexLabel(String).make()` on an open +graph or management transaction and provide the name of the vertex label +as the argument. Vertex label names must be unique in the graph. + +```java +mgmt = graph.openManagement() +person = mgmt.makeVertexLabel('person').make() +mgmt.commit() +// Create a labeled vertex +person = graph.addVertex(label, 'person') +// Create an unlabeled vertex +v = graph.addVertex() +graph.tx().commit() +``` + +## Automatic Schema Maker + +If an edge label, property key, or vertex label has not been defined +explicitly, it will be defined implicitly when it is first used during +the addition of an edge, vertex or the setting of a property. The +`DefaultSchemaMaker` configured for the JanusGraph graph defines such +types. + +By default, implicitly created edge labels have multiplicity MULTI and +implicitly created property keys have cardinality SINGLE and data type +`Object.class`. Users can control automatic schema element creation by +implementing and registering their own `DefaultSchemaMaker`. + +It is strongly encouraged to explicitly define all schema elements and +to disable automatic schema creation by setting `schema.default=none` in +the JanusGraph graph configuration. + +## Changing Schema Elements + +The definition of an edge label, property key, or vertex label cannot be +changed once its committed into the graph. However, the names of schema +elements can be changed via +`JanusGraphManagement.changeName(JanusGraphSchemaElement, String)` as +shown in the following example where the property key `place` is renamed +to `location`. + +```java +mgmt = graph.openManagement() +place = mgmt.getPropertyKey('place') +mgmt.changeName(place, 'location') +mgmt.commit() +``` + +Note, that schema name changes may not be immediately visible in +currently running transactions and other JanusGraph graph instances in +the cluster. While schema name changes are announced to all JanusGraph +instances through the storage backend, it may take a while for the +schema changes to take effect and it may require a instance restart in +the event of certain failure conditions - like network partitions - if +they coincide with the rename. Hence, the user must ensure that either +of the following holds: + +- The renamed label or key is not currently in active use (i.e. + written or read) and will not be in use until all JanusGraph + instances are aware of the name change. +- Running transactions actively accommodate the brief intermediate + period where either the old or new name is valid based on the + specific JanusGraph instance and status of the name-change + announcement. For instance, that could mean transactions query for + both names simultaneously. + +Should the need arise to re-define an existing schema type, it is +recommended to change the name of this type to a name that is not +currently (and will never be) in use. After that, a new label or key can +be defined with the original name, thereby effectively replacing the old +one. However, note that this would not affect vertices, edges, or +properties previously written with the existing type. Redefining +existing graph elements is not supported online and must be accomplished +through a batch graph transformation. diff --git a/docs/basics/server.md b/docs/basics/server.md new file mode 100644 index 0000000000..8e2761f89b --- /dev/null +++ b/docs/basics/server.md @@ -0,0 +1,469 @@ +# JanusGraph Server +JanusGraph uses the [Gremlin Server](http://tinkerpop.apache.org/docs/{{tinkerpop_version}}/reference/#gremlin-server) +engine as the server component to process and answer client queries. +When packaged in JanusGraph, Gremlin Server is called JanusGraph Server. + +JanusGraph Server must be started manually in order to use it. +JanusGraph Server provides a way to remotely execute Gremlin traversals +against one or more JanusGraph instances hosted within it. This section +will describe how to use the WebSocket configuration, as well as +describe how to configure JanusGraph Server to handle HTTP endpoint +interactions. For information about how to connect to a JanusGraph +Server from different languages refer to [Connecting to JanusGraph](../connecting/index.md). + +## Getting Started + +### Using the Pre-Packaged Distribution + +The JanusGraph +[release](https://github.com/JanusGraph/janusgraph/releases) comes +pre-configured to run JanusGraph Server out of the box leveraging a +sample Cassandra and Elasticsearch configuration to allow users to get +started quickly with JanusGraph Server. This configuration defaults to +client applications that can connect to JanusGraph Server via WebSocket +with a custom subprotocol. There are a number of clients developed in +different languages to help support the subprotocol. The most familiar +client to use the WebSocket interface is the Gremlin Console. The +quick-start bundle is not intended to be representative of a production +installation, but does provide a way to perform development with +JanusGraph Server, run tests and see how the components are wired +together. To use this default configuration: + +- Download a copy of the current `janusgraph-$VERSION.zip` file from + the [Releases + page](https://github.com/JanusGraph/janusgraph/releases) + +- Unzip it and enter the `janusgraph-$VERSION` directory + +- Run `bin/janusgraph.sh start`. This step will start Gremlin Server + with Cassandra/ES forked into a separate process. Note for security + reasons Elasticsearch and therefore `janusgraph.sh` must be run + under a non-root account. + + +```bash +$ bin/janusgraph.sh start +Forking Cassandra... +Running `nodetool statusthrift`.. OK (returned exit status 0 and printed string "running"). +Forking Elasticsearch... +Connecting to Elasticsearch (127.0.0.1:9300)... OK (connected to 127.0.0.1:9300). +Forking Gremlin-Server... +Connecting to Gremlin-Server (127.0.0.1:8182)... OK (connected to 127.0.0.1:8182). +Run gremlin.sh to connect. +``` + +#### Connecting to Gremlin Server + +After running `janusgraph.sh`, Gremlin Server will be ready to listen +for WebSocket connections. The easiest way to test the connection is +with Gremlin Console. + +Start Gremlin Console with bin/gremlin.sh and use the :remote and :> commands to issue Gremlin to Gremlin Server: + +```bash +$ bin/gremlin.sh + \,,,/ + (o o) +-----oOOo-(3)-oOOo----- +plugin activated: tinkerpop.server +plugin activated: tinkerpop.hadoop +plugin activated: tinkerpop.utilities +plugin activated: janusgraph.imports +plugin activated: tinkerpop.tinkergraph +gremlin> :remote connect tinkerpop.server conf/remote.yaml +==>Connected - localhost/127.0.0.1:8182 +gremlin> :> graph.addVertex("name", "stephen") +==>v[256] +gremlin> :> g.V().values('name') +==>stephen +``` + +The `:remote` command tells the console to configure a remote connection +to Gremlin Server using the `conf/remote.yaml` file to connect. That +file points to a Gremlin Server instance running on `localhost`. The +`:>` is the "submit" command which sends the Gremlin on that line to the +currently active remote. By default remote conenctions are sessionless, +meaning that each line sent in the console is interpreted as a single +request. Multiple statements can be sent on a single line using a +semicolon as the delimiter. Alternately, you can establish a console +with a session by specifying +[session](https://tinkerpop.apache.org/docs/current/reference/#sessions) +when creating the connection. A [console +session](https://tinkerpop.apache.org/docs/current/reference/#console-sessions) +allows you to reuse variables across several lines of input. +```groovy +gremlin> :remote connect tinkerpop.server conf/remote.yaml +==>Configured localhost/127.0.0.1:8182 +gremlin> graph +==>standardjanusgraph[cql:[127.0.0.1]] +gremlin> g +==>graphtraversalsource[standardjanusgraph[cql:[127.0.0.1]], standard] +gremlin> g.V() +gremlin> user = "Chris" +==>Chris +gremlin> graph.addVertex("name", user) +No such property: user for class: Script21 +Type ':help' or ':h' for help. +Display stack trace? [yN] +gremlin> :remote connect tinkerpop.server conf/remote.yaml session +==>Configured localhost/127.0.0.1:8182-[9acf239e-a3ed-4301-b33f-55c911e04052] +gremlin> g.V() +gremlin> user = "Chris" +==>Chris +gremlin> user +==>Chris +gremlin> graph.addVertex("name", user) +==>v[4344] +gremlin> g.V().values('name') +==>Chris +``` + +## Cleaning up after the Pre-Packaged Distribution + +If you want to start fresh and remove the database and logs you can use +the clean command with `janusgraph.sh`. The server should be stopped +before running the clean operation. +```bash +$ cd /Path/to/janusgraph/janusgraph-0.2.0-hadoop2/ +$ ./bin/janusgraph.sh stop +Killing Gremlin-Server (pid 91505)... +Killing Elasticsearch (pid 91402)... +Killing Cassandra (pid 91219)... +$ ./bin/janusgraph.sh clean +Are you sure you want to delete all stored data and logs? [y/N] y +Deleted data in /Path/to/janusgraph/janusgraph-0.2.0-hadoop2/db +Deleted logs in /Path/to/janusgraph/janusgraph-0.2.0-hadoop2/log +``` + +## JanusGraph Server as a WebSocket Endpoint + +The default configuration described in [Getting Started](#getting-started) +is already a WebSocket configuration. +If you want to alter the default configuration to work with your own +Cassandra or HBase environment rather than use the quick start +environment, follow these steps: + +**To Configure JanusGraph Server For WebSocket** + +1. Test a local connection to a JanusGraph database first. This step + applies whether using the Gremlin Console to test the connection, or + whether connecting from a program. Make appropriate changes in a + properties file in the `./conf` directory for your environment. For + example, edit `./conf/janusgraph-hbase.properties` and make sure the + storage.backend, storage.hostname and storage.hbase.table parameters + are specified correctly. For more information on configuring + JanusGraph for various storage backends, see + [Storage Backends](../storage-backend/index.md). Make sure the properties file contains the + following line: +```conf +gremlin.graph=org.janusgraph.core.JanusGraphFactory +``` + +2. Once a local configuration is tested and you have a working + properties file, copy the properties file from the `./conf` + directory to the `./conf/gremlin-server` directory. +```bash +cp conf/janusgraph-hbase.properties +conf/gremlin-server/socket-janusgraph-hbase-server.properties +``` + +3. Copy `./conf/gremlin-server/gremlin-server.yaml` to a new file + called `socket-gremlin-server.yaml`. Do this in case you need to + refer to the original version of the file +```bash +cp conf/gremlin-server/gremlin-server.yaml +conf/gremlin-server/socket-gremlin-server.yaml +``` + +4. Edit the `socket-gremlin-server.yaml` file and make the following + updates: + + 1. If you are planning to connect to JanusGraph Server from + something other than localhost, update the IP address for host: +```conf +host: 10.10.10.100 +``` + + 2. Update the graphs section to point to your new properties file + so the JanusGraph Server can find and connect to your JanusGraph + instance: +```yaml +graphs: { graph: + conf/gremlin-server/socket-janusgraph-hbase-server.properties} +``` + +5. Start the JanusGraph Server, specifying the yaml file you just + configured: +```bash +bin/gremlin-server.sh ./conf/gremlin-server/socket-gremlin-server.yaml +``` + +6. The JanusGraph Server should now be running in WebSocket mode and + can be tested by following the instructions in [Connecting to Gremlin Server](#first-example-connecting-gremlin-server) + +!!! Important + Do not use `bin/janusgraph.sh`. That starts the default + configuration, which starts a separate Cassandra/Elasticsearch + environment. + +## JanusGraph Server as a HTTP Endpoint + +The default configuration described in [Getting Started](#getting-started) is a WebSocket configuration. If you +want to alter the default configuration in order to use JanusGraph +Server as an HTTP endpoint for your JanusGraph database, follow these +steps: + +1. Test a local connection to a JanusGraph database first. This step + applies whether using the Gremlin Console to test the connection, or + whether connecting from a program. Make appropriate changes in a + properties file in the `./conf` directory for your environment. For + example, edit `./conf/janusgraph-hbase.properties` and make sure the + storage.backend, storage.hostname and storage.hbase.table parameters + are specified correctly. For more information on configuring + JanusGraph for various storage backends, see + [Storage Backends](../storage-backend/index.md). Make sure the properties file contains the + following line: +```conf +gremlin.graph=org.janusgraph.core.JanusGraphFactory +``` + +2. Once a local configuration is tested and you have a working + properties file, copy the properties file from the `./conf` + directory to the `./conf/gremlin-server` directory. +```bash +cp conf/janusgraph-hbase.properties conf/gremlin-server/http-janusgraph-hbase-server.properties +``` + +3. Copy `./conf/gremlin-server/gremlin-server.yaml` to a new file + called `http-gremlin-server.yaml`. Do this in case you need to refer + to the original version of the file +```bash +cp conf/gremlin-server/gremlin-server.yaml conf/gremlin-server/http-gremlin-server.yaml +``` + +4. Edit the `http-gremlin-server.yaml` file and make the following + updates: + + 1. If you are planning to connect to JanusGraph Server from + something other than localhost, update the IP address for host: +```conf +host: 10.10.10.100 +``` + + 2. Update the channelizer setting to specify the HttpChannelizer: +```yaml +channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer +``` + + 3. Update the graphs section to point to your new properties file + so the JanusGraph Server can find and connect to your JanusGraph + instance: +```yaml +graphs: { graph: + conf/gremlin-server/http-janusgraph-hbase-server.properties} +``` + +5. Start the JanusGraph Server, specifying the yaml file you just + configured: +```bash +bin/gremlin-server.sh ./conf/gremlin-server/http-gremlin-server.yaml +``` + +6. The JanusGraph Server should now be running in HTTP mode and + available for testing. **curl** can be used to verify the server is + working: +```bash +curl -XPOST -Hcontent-type:application/json -d *{"gremlin":"g.V().count()"}* [IP for JanusGraph server host](http://):8182 +``` + +## JanusGraph Server as Both a WebSocket and HTTP Endpoint + +As of JanusGraph 0.2.0, you can configure your `gremlin-server.yaml` to +accept both WebSocket and HTTP connections over the same port. This can +be achieved by changing the channelizer in any of the previous examples +as follows. +```yaml +channelizer: org.apache.tinkerpop.gremlin.server.channel.WsAndHttpChannelizer +``` +## Advanced JanusGraph Server Configurations +### WebSocket versus HTTP + +JanusGraph Server must be run with a configuration for either WebSocket (for Gremlin Console or other WebSocket programs) or re-configured for HTTP to work with HTTP clients. + +Currently, there is no way to configure a single running instance of JanusGraph Server to communicate simultaneously with clients talking WebSocket and HTTP. However, it is possible to create configuration files (following the steps in this chapter) for both WebSocket and HTTP and start two instances of the JanusGraph Server by pointing it to the various configurations when it is started. In this way, the same JanusGraph database instance can be reached through different JanusGraph Servers through both WebSocket and HTTP calls. Make sure the port parameter in each yaml configuration is different if starting more than one JanusGraph Server on the same machine: + +``` +port: 8182 +``` + +In the future, the option to have one running instance of JanusGraph Server supporting multiple protocols may be possible. + +### Using TinkerPop Gremlin Server with JanusGraph + +Since JanusGraph Server is a TinkerPop Gremlin Server packaged +with configuration files for JanusGraph, a version compatible +TinkerPop Gremlin Server can be downloaded separately and used with JanusGraph. +Get started by downloading the appropriate version of Gremlin Server, +which needs to match a version supported by the JanusGraph version in use ({{ tinkerpop_version }}). + +!!! important + Any references to file paths in this section refer to paths under a + TinkerPop distribution for Gremlin Server and not a JanusGraph + distribution with the JanusGraph Server, unless specifically noted. + +Configuring a standalone Gremlin Server to work with +JanusGraph is similar to configuring the packaged JanusGraph Server. +You should be familiar with [graph configuration](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference/#_configuring_2). +Basically, the Gremlin Server yaml file points to graph-specific configuration files +that are used to instantiate JanusGraph instances that it will then host. +In order to instantiate these Graph instances, +Gremlin Server requires that the appropriate libraries and dependencies +for the JanusGraph be available on its classpath. + +For purposes of demonstration, these instructions will outline how to +configure the BerkeleyDB backend for JanusGraph in Gremlin Server. As +stated earlier, Gremlin Server needs JanusGraph dependencies on its +classpath. Invoke the following command replacing `$VERSION` with the +version of JanusGraph to use: + +```bash +bin/gremlin-server.sh -i org.janusgraph janusgraph-all $VERSION +``` + +When this process completes, Gremlin Server should now have all the +JanusGraph dependencies available to it and will thus be able to +instantiate `JanusGraph` objects. + +!!! important + The above command uses Groovy Grape and if it is not configured properly + download errors may ensue. Please refer to [this section](http://tinkerpop.apache.org/docs/{{tinkerpop_version}} /reference/#gremlin-applications) + of the TinkerPop documentation for more information around setting up ~/.groovy/grapeConfig.xml. + +Create a file called `GREMLIN_SERVER_HOME/conf/janusgraph.properties` +with the following contents: +```conf +gremlin.graph=org.janusgraph.core.JanusGraphFactory +storage.backend=berkeleyje +storage.directory=db/berkeley +``` + +Configuration of other backends is similar. See +[Storage Backends](../storage-backend/index.md). If using Cassandra, then use Cassandra +configuration options in the `janusgraph.properties` file. The only +important piece to leave unchanged is the `gremlin.graph` setting which +should always use `JanusGraphFactory`. This setting tells Gremlin Server +how to instantiate a `JanusGraph` instance. + +Next create a file called +`GREMLIN_SERVER_HOME/conf/gremlin-server-janusgraph.yaml` that has the +following contents: +```yaml +host: localhost +port: 8182 +graphs: { + graph: conf/janusgraph.properties} +plugins: + - janusgraph.imports +scriptEngines: { + gremlin-groovy: { + scripts: [scripts/janusgraph.groovy]}} +serializers: + - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }} + - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { serializeResultToString: true }} +metrics: { + slf4jReporter: {enabled: true, interval: 180000}} +``` + +**There are several important parts to this configuration file as they relate to JanusGraph.** + +1. In the `graphs` map, there is a key called `graph` and its value is + `conf/janusgraph.properties`. This tells Gremlin Server to + instantiate a `Graph` instance called "graph" and use the + `conf/janusgraph.properties` file to configure it. The "graph" key + becomes the unique name for the `Graph` instance in Gremlin Server + and it can be referenced as such in the scripts submitted to it. + +2. In the `plugins` list, there is a reference to + `janusgraph.imports`, which tells Gremlin Server to initialize + the "JanusGraph Plugin". The "JanusGraph Plugin" will auto-import + JanusGraph specific classes for usage in scripts. + +3. Note the `scripts` key and the reference to + `scripts/janusgraph.groovy`. This Groovy file is an initialization + script for Gremlin Server and that particular ScriptEngine. Create + `scripts/janusgraph.groovy` with the following contents: + +```groovy +def globals = [:] +globals << [g : graph.traversal()] +``` + +The above script creates a `Map` called `globals` and assigns to it a +key/value pair. The key is `g` and its value is a `TraversalSource` +generated from `graph`, which was configured for Gremlin Server in its +configuration file. At this point, there are now two global variables +available to scripts provided to Gremlin Server - `graph` and `g`. + +At this point, Gremlin Server is configured and can be used to connect +to a new or existing JanusGraph database. To start the server: +```bash +$ bin/gremlin-server.sh conf/gremlin-server-janusgraph.yaml +[INFO] GremlinServer - + \,,,/ + (o o) +-----oOOo-(3)-oOOo----- + +[INFO] GremlinServer - Configuring Gremlin Server from conf/gremlin-server-janusgraph.yaml +[INFO] MetricManager - Configured Metrics Slf4jReporter configured with interval=180000ms and loggerName=org.apache.tinkerpop.gremlin.server.Settings$Slf4jReporterMetrics +[INFO] GraphDatabaseConfiguration - Set default timestamp provider MICRO +[INFO] GraphDatabaseConfiguration - Generated unique-instance-id=7f0000016240-ubuntu1 +[INFO] Backend - Initiated backend operations thread pool of size 8 +[INFO] KCVSLog$MessagePuller - Loaded unidentified ReadMarker start time 2015-10-02T12:28:24.411Z into org.janusgraph.diskstorage.log.kcvs.KCVSLog$MessagePuller@35399441 +[INFO] GraphManager - Graph [graph] was successfully configured via [conf/janusgraph.properties]. +[INFO] ServerGremlinExecutor - Initialized Gremlin thread pool. Threads in pool named with pattern gremlin-* +[INFO] ScriptEngines - Loaded gremlin-groovy ScriptEngine +[INFO] GremlinExecutor - Initialized gremlin-groovy ScriptEngine with scripts/janusgraph.groovy +[INFO] ServerGremlinExecutor - Initialized GremlinExecutor and configured ScriptEngines. +[INFO] ServerGremlinExecutor - A GraphTraversalSource is now bound to [g] with graphtraversalsource[standardjanusgraph[berkeleyje:db/berkeley], standard] +[INFO] AbstractChannelizer - Configured application/vnd.gremlin-v1.0+gryo with org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0 +[INFO] AbstractChannelizer - Configured application/vnd.gremlin-v1.0+gryo-stringd with org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0 +[INFO] GremlinServer$1 - Gremlin Server configured with worker thread pool of 1, gremlin pool of 8 and boss thread pool of 1. +[INFO] GremlinServer$1 - Channel started at port 8182. +``` + +The following section explains how to connect to the running server. + +#### Connecting to JanusGraph via Gremlin Server + +Gremlin Server will be ready to listen for WebSocket connections when it +is started. The easiest way to test the connection is with Gremlin +Console. + +Follow the instructions here [Connecting to Gremlin Server](#first-example-connecting-gremlin-server) to verify the Gremlin +Server is working. + +!!! important + A difference you should understand is that when working with + JanusGraph Server, the Gremlin Console is started from underneath the + JanusGraph distribution and when following the test instructions here + for a standalone Gremlin Server, the Gremlin Console is started from + under the TinkerPop distribution. + +```java +GryoMapper mapper = GryoMapper.build().addRegistry(JanusGraphIoRegistry.INSTANCE).create(); +Cluster cluster = Cluster.build().serializer(new GryoMessageSerializerV3d0(mapper)).create(); +Client client = cluster.connect(); +client.submit("g.V()").all().get(); +``` + +By adding the `JanusGraphIoRegistry` to the +`org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0`, the +driver will know how to properly deserialize custom data types returned +by JanusGraph. + +## Extending JanusGraph Server + +It is possible to extend Gremlin Server with other means of +communication by implementing the interfaces that it provides and +leverage this with JanusGraph. See more details in the appropriate +TinkerPop documentation. diff --git a/docs/basics/technical-limitations.md b/docs/basics/technical-limitations.md new file mode 100644 index 0000000000..df7d9e739f --- /dev/null +++ b/docs/basics/technical-limitations.md @@ -0,0 +1,104 @@ +Technical Limitations +===================== + +There are various limitations and "gotchas" that one should be aware of +when using JanusGraph. Some of these limitations are necessary design +choices and others are issues that will be rectified as JanusGraph +development continues. Finally, the last section provides solutions to +common issues. + +Design Limitations +------------------ + +These limitations reflect long-term tradeoffs design tradeoffs which are +either difficult or impractical to change. These limitations are +unlikely to be removed in the near future. + +### Size Limitation + +JanusGraph can store up to a quintillion edges (2^60) and half as many +vertices. That limitation is imposed by JanusGraph’s id scheme. + +### DataType Definitions + +When declaring the data type of a property key using `dataType(Class)` +JanusGraph will enforce that all properties for that key have the +declared type, unless that type is `Object.class`. This is an equality +type check, meaning that sub-classes will not be allowed. For instance, +one cannot declare the data type to be `Number.class` and use `Integer` +or `Long`. For efficiency reasons, the type needs to match exactly. +Hence, use `Object.class` as the data type for type flexibility. In all +other cases, declare the actual data type to benefit from increased +performance and type safety. + +### Edge Retrievals are O(log(k)) + +Retrieving an edge by id, e.g `tx.getEdge(edge.getId())`, is not a +constant time operation because it requires an index call on one of its +adjacent vertices. Hence, the cost of retrieving an individual edge by +its id is `O(log(k))` where `k` is the number of incident edges on the +adjacent vertex. JanusGraph will attempt to pick the adjacent vertex +with the smaller degree. + +This also applies to index retrievals for edges via a standard or +external index. + +### Type Definitions cannot be changed + +The definition of an edge label, property key, or vertex label cannot be +changed once it has been committed to the graph. However, a type can be +renamed and new types can be created at runtime to accommodate an +evolving schema. + +### Reserved Keywords + +There are certain keywords that JanusGraph uses internally for types +that cannot be used otherwise. These types include vertex labels, edge +labels, and property keys. The following are keywords that cannot be +used: + +- vertex + +- element + +- edge + +- property + +- label + +- key + +For example, if you attempt to create a vertex with the label of +`property`, you will receive an exception regarding protected system +types. + +Temporary Limitations +--------------------- + +These are limitations in JanusGraph’s current implementation. These +limitations could reasonably be removed in upcoming versions of +JanusGraph. + +### Limited Mixed Index Support + +Mixed indexes only support a subset of the data types that JanusGraph +supports. See [Mixed Index Data Types](../index-backend/search-predicates.md#data-type-support) for a current +listing. Also, mixed indexes do not currently support property keys with +SET or LIST cardinality. + +### Batch Loading Speed + +JanusGraph provides a batch loading mode that can be enabled through the +[graph configuration](configuration-reference.md). However, this batch mode only +facilitates faster loading into the storage backend, it does not use +storage backend specific batch loading techniques that prepare the data +in memory for disk storage. As such, batch loading in JanusGraph is +currently slower than batch loading modes provided by single machine +databases. [Bulk Loading](../advanced-topics/bulk-loading.md) contains information on speeding up +batch loading in JanusGraph. + +Another limitation related to batch loading is the failure to load +millions of edges into a single vertex at once or in a short time of +period. Such **supernode loading** can fail for some storage backends. +This limitation also applies to dense index entries. diff --git a/docs/basics/transaction-log.md b/docs/basics/transaction-log.md new file mode 100644 index 0000000000..9d4a3b1794 --- /dev/null +++ b/docs/basics/transaction-log.md @@ -0,0 +1,194 @@ +Transaction Log +=============== + +JanusGraph can automatically log transactional changes for additional +processing or as a record of change. To enable logging for a particular +transaction, specify the name of the target log during the start of the +transaction. +```groovy +tx = graph.buildTransaction().logIdentifier('addedPerson').start() +u = tx.addVertex(label, 'human') +u.property('name', 'proteros') +u.property('age', 36) +tx.commit() +``` + +Upon commit, any changes made during the transaction are logged to the +user logging system into a log named `addedPerson`. The **user logging +system** is a configurable logging backend with a JanusGraph compatible +log interface. By default, the log is written to a separate store in the +primary storage backend which can be configured as described below. The +log identifier specified during the start of the transaction identifies +the log in which the changes are recorded thereby allowing different +types of changes to be recorded in separate logs for individual +processing. +```groovy +tx = graph.buildTransaction().logIdentifier('battle').start() +h = tx.traversal().V().has('name', 'hercules').next() +m = tx.addVertex(label, 'monster') +m.property('name', 'phylatax') +h.addEdge('battled', m, 'time', 22) +tx.commit() +``` + +JanusGraph provides a user transaction log processor framework to +process the recorded transactional changes. The transaction log +processor is opened via +`JanusGraphFactory.openTransactionLog(JanusGraph)` against a previously +opened JanusGraph graph instance. One can then add processors for a +particular log which holds transactional changes. +```groovy +import java.util.concurrent.atomic.*; +import org.janusgraph.core.log.*; +import java.util.concurrent.*; +logProcessor = JanusGraphFactory.openTransactionLog(g); +totalHumansAdded = new AtomicInteger(0); +totalGodsAdded = new AtomicInteger(0); +logProcessor.addLogProcessor("addedPerson"). + setProcessorIdentifier("addedPersonCounter"). + setStartTimeNow(). + addProcessor(new ChangeProcessor() { + @Override + public void process(JanusGraphTransaction tx, TransactionId txId, ChangeState changeState) { + for (v in changeState.getVertices(Change.ADDED)) { + if (v.label().equals("human")) totalHumansAdded.incrementAndGet(); + } + } + }). + addProcessor(new ChangeProcessor() { + @Override + public void process(JanusGraphTransaction tx, TransactionId txId, ChangeState changeState) { + for (v in changeState.getVertices(Change.ADDED)) { + if (v.label().equals("god")) totalGodsAdded.incrementAndGet(); + } + } + }). + build(); +``` + +In this example, a **log processor** is built for the user transaction +log named `addedPerson` to process the changes made in transactions +which used the `addedPerson` log identifier. Two **change processors** +are added to this log processor. The first processor counts the number +of humans added and the second counts the number of gods added to the +graph. + +When a log processor is built against a particular log, such as the +`addedPerson` log in the example above, it will start reading +transactional change records from the log immediately upon successful +construction and initialization up to the head of the log. The start +time specified in the builder marks the time point in the log where the +log processor will start reading records. Optionally, one can specify an +identifier for the log processor in the builder. The log processor will +use the identifier to regularly persist its state of processing, i.e. it +will maintain a marker on the last read log record. If the log processor +is later restarted with the same identifier, it will continue reading +from the last read record. This is particularly useful when the log +processor is supposed to run for long periods of time and is therefore +likely to fail. In such failure situations, the log processor can simply +be restarted with the same identifier. It must be ensured that log +processor identifiers are unique in a JanusGraph cluster in order to +avoid conflicts on the persisted read markers. + +A change processor must implement the `ChangeProcessor` interface. It’s +`process()` method is invoked for each change record read from the log +with a `JanusGraphTransaction` handle, the id of the transaction that +caused the change, and a `ChangeState` container which holds the +transactional changes. The change state container can be queried to +retrieve individual elements that were part of the change state. In the +example, all added vertices are retrieved. Refer to the API +documentation for a description of all the query methods on +`ChangeState`. The provided transaction id can be used to investigate +the origin of the transaction which is uniquely identified by the +combination of the id of the JanusGraph instance that executed the +transaction (`txId.getInstanceId()`) and the instance specific +transaction id (`txId.getTransactionId()`). In addition, the time of the +transaction is available through `txId.getTransactionTime()`. + +Change processors are executed individually and in multiple threads. If +a change processor accesses global state it must be ensured that such +state allows concurrent access. While the log processor reads log +records sequentially, the changes are processed in multiple threads so +it cannot be guaranteed that the log order is preserved in the change +processors. + +Note, that log processors run each registered change processor at least +once for each record in the log which means that a single transactional +change record may be processed multiple times under certain failure +conditions. One cannot add or remove change processor from a running log +processor. In other words, a log processor is immutable after it is +built. To change log processing, start a new log processor and shut down +an existing one. +```groovy +logProcessor.addLogProcessor("battle"). + setProcessorIdentifier("battleTimer"). + setStartTimeNow(). + addProcessor(new ChangeProcessor() { + @Override + public void process(JanusGraphTransaction tx, TransactionId txId, ChangeState changeState) { + h = tx.V().has("name", "hercules").toList().iterator().next(); + for (edge in changeState.getEdges(h, Change.ADDED, Direction.OUT, "battled")) { + if (edge.value("time")>1000) + h.property("oldFighter", true); + } + } + }). + build(); +``` + +The log processor above processes transactions for the `battle` log +identifier with a single change processor which evaluates `battled` +edges that were added to Hercules. This example demonstrates that the +transaction handle passed into the change processor is a normal +`JanusGraphTransaction` which query the JanusGraph graph and make +changes to it. + +Transaction Log Use Cases +------------------------- + +### Record of Change + +The user transaction log can be used to keep a record of all changes +made against the graph. By using separate log identifiers, changes can +be recorded in different logs to distinguish separate transaction types. + +At any time, a log processor can be built which can processes all +recorded changes starting from the desired start time. This can be used +for forensic analysis, to replay changes against a different graph, or +to compute an aggregate. + +### Downstream Updates + +It is often the case that a JanusGraph graph cluster is part of a larger +architecture. The user transaction log and the log processor framework +provide the tools needed to broadcast changes to other components of the +overall system without slowing down the original transactions causing +the change. This is particularly useful when transaction latencies need +to be low and/or there are a number of other systems that need to be +alerted to a change in the graph. + +### Triggers + +The user transaction log provides the basic infrastructure to implement +triggers that can scale to a large number of concurrent transactions and +very large graphs. A trigger is registered with a particular change of +data and either triggers an event in an external system or additional +changes to the graph. At scale, it is not advisable to implement +triggers in the original transaction but rather process triggers with a +slight delay through the log processor framework. The second example +shows how changes to the graph can be evaluated and trigger additional +modifications. + +Log Configuration +----------------- + +There are a number of configuration options to fine tune how the log +processor reads from the log. Refer to the complete list of +configuration options [Configuration Reference](configuration-reference.md) for the +options under the `log` namespace. To configure the user transaction +log, use the `log.user` namespace. The options listed there allow the +configuration of the number of threads to be used, the number of log +records read in each batch, the read interval, and whether the +transaction change records should automatically expire and be removed +from the log after a configurable amount of time (TTL). + diff --git a/docs/basics/transactions.md b/docs/basics/transactions.md new file mode 100644 index 0000000000..aef5a207b4 --- /dev/null +++ b/docs/basics/transactions.md @@ -0,0 +1,324 @@ +Transactions +============ + +Almost all interaction with JanusGraph is associated with a transaction. +JanusGraph transactions are safe for concurrent use by multiple threads. +Methods on a JanusGraph instance like `graph.V(...)` and +`graph.tx().commit()` perform a `ThreadLocal` lookup to retrieve or +create a transaction associated with the calling thread. Callers can +alternatively forego `ThreadLocal` transaction management in favor of +calling `graph.tx().createThreadedTx()`, which returns a reference to a +transaction object with methods to read/write graph data and commit or +rollback. + +JanusGraph transactions are not necessarily ACID. They can be so +configured on BerkeleyDB, but they are not generally so on Cassandra or +HBase, where the underlying storage system does not provide serializable +isolation or multi-row atomic writes and the cost of simulating those +properties would be substantial. + +This section describes JanusGraph’s transactional semantics and API. + +Transaction Handling +-------------------- + +Every graph operation in JanusGraph occurs within the context of a +transaction. According to the TinkerPop’s transactional specification, +each thread opens its own transaction against the graph database with +the first operation (i.e. retrieval or mutation) on the graph: +```groovy +graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph") +juno = graph.addVertex() //Automatically opens a new transaction +juno.property("name", "juno") +graph.tx().commit() //Commits transaction +``` + +In this example, a local JanusGraph graph database is opened. Adding the +vertex "juno" is the first operation (in this thread) which +automatically opens a new transaction. All subsequent operations occur +in the context of that same transaction until the transaction is +explicitly stopped or the graph database is closed. If transactions are +still open when `close()` is called, then the behavior of the +outstanding transactions is technically undefined. In practice, any +non-thread-bound transactions will usually be effectively rolled back, +but the thread-bound transaction belonging to the thread that invoked +shutdown will first be committed. Note, that both read and write +operations occur within the context of a transaction. + +Transactional Scope +------------------- + +All graph elements (vertices, edges, and types) are associated with the +transactional scope in which they were retrieved or created. Under +TinkerPop’s default transactional semantics, transactions are +automatically created with the first operation on the graph and closed +explicitly using `commit()` or `rollback()`. Once the transaction is +closed, all graph elements associated with that transaction become stale +and unavailable. However, JanusGraph will automatically transition +vertices and types into the new transactional scope as shown in this +example: +```groovy +graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph") +juno = graph.addVertex() //Automatically opens a new transaction +graph.tx().commit() //Ends transaction +juno.property("name", "juno") //Vertex is automatically transitioned +``` + +Edges, on the other hand, are not automatically transitioned and cannot +be accessed outside their original transaction. They must be explicitly +transitioned: +```groovy +e = juno.addEdge("knows", graph.addVertex()) +graph.tx().commit() //Ends transaction +e = g.E(e).next() //Need to refresh edge +e.property("time", 99) +``` + +Transaction Failures +-------------------- + +When committing a transaction, JanusGraph will attempt to persist all +changes to the storage backend. This might not always be successful due +to IO exceptions, network errors, machine crashes or resource +unavailability. Hence, transactions can fail. In fact, transactions +**will eventually fail** in sufficiently large systems. Therefore, we +highly recommend that your code expects and accommodates such failures: +```groovy +try { + if (g.V().has("name", name).iterator().hasNext()) + throw new IllegalArgumentException("Username already taken: " + name) + user = graph.addVertex() + user.property("name", name) + graph.tx().commit() +} catch (Exception e) { + //Recover, retry, or return error message + println(e.getMessage()) +} +``` + +The example above demonstrates a simplified user signup implementation +where `name` is the name of the user who wishes to register. First, it +is checked whether a user with that name already exists. If not, a new +user vertex is created and the name assigned. Finally, the transaction +is committed. + +If the transaction fails, a `JanusGraphException` is thrown. There are a +variety of reasons why a transaction may fail. JanusGraph differentiates +between *potentially temporary* and *permanent* failures. + +Potentially temporary failures are those related to resource +unavailability and IO hiccups (e.g. network timeouts). JanusGraph +automatically tries to recover from temporary failures by retrying to +persist the transactional state after some delay. The number of retry +attempts and the retry delay are configurable (see [Configuration Reference](configuration-reference.md)). + +Permanent failures can be caused by complete connection loss, hardware +failure or lock contention. To understand the cause of lock contention, +consider the signup example above and suppose a user tries to signup +with username "juno". That username may still be available at the +beginning of the transaction but by the time the transaction is +committed, another user might have concurrently registered with "juno" +as well and that transaction holds the lock on the username therefore +causing the other transaction to fail. Depending on the transaction +semantics one can recover from a lock contention failure by re-running +the entire transaction. + +Permanent exceptions that can fail a transaction include: + +- PermanentLockingException(**Local lock contention**): Another local + thread has already been granted a conflicting lock. + +- PermanentLockingException(**Expected value mismatch for X: + expected=Y vs actual=Z**): The verification that the value read in + this transaction is the same as the one in the datastore after + applying for the lock failed. In other words, another transaction + modified the value after it had been read and modified. + +Multi-Threaded Transactions +--------------------------- + +JanusGraph supports multi-threaded transactions through TinkerPop’s threaded transactions. + Hence, to speed up transaction processing and utilize multi-core architectures multiple threads can run concurrently in a single transaction. + +With TinkerPop’s default transaction handling, each thread automatically +opens its own transaction against the graph database. To open a +thread-independent transaction, use the `createThreadedTx()` method. +```groovy +threadedGraph = graph.tx().createThreadedTx(); +threads = new Thread[10]; +for (int i=0; i> returns nothing, v has no edges +//thread is idle for a few seconds, another thread adds edges to v +g.V(v).bothE() +>> still returns nothing because the transactional state from the beginning is maintained +``` + +Such unexpected behavior is likely to occur in client-server +applications where the server maintains multiple threads to answer +client requests. It is therefore important to terminate the transaction +after a unit of work (e.g. code snippet, query, etc). So, the example +above should be: + +```groovy +v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction +g.V(v).bothE() +graph.tx().commit() +//thread is idle for a few seconds, another thread adds edges to v +g.V(v).bothE() +>> returns the newly added edge +graph.tx().commit() +``` + +When using multi-threaded transactions via `newTransaction` all vertices +and edges retrieved or created in the scope of that transaction are +**not** available outside the scope of that transaction. Accessing such +elements after the transaction has been closed will result in an +exception. As demonstrated in the example above, such elements have to +be explicitly refreshed in the new transaction using +`g.V(existingVertex)` or `g.E(existingEdge)`. + +Transaction Configuration +------------------------- + +JanusGraph’s `JanusGraph.buildTransaction()` method gives the user the +ability to configure and start a new [multi-threaded transaction](#multi-threaded-transactions) +against a `JanusGraph`. Hence, it is identical to `JanusGraph.newTransaction()` with additional configuration +options. + +`buildTransaction()` returns a `TransactionBuilder` which allows the +following aspects of a transaction to be configured: + +- `readOnly()` - makes the transaction read-only and any attempt to + modify the graph will result in an exception. + +- `enableBatchLoading()` - enables batch-loading for an individual + transaction. This setting results in similar efficiencies as the + graph-wide setting `storage.batch-loading` due to the disabling of + consistency checks and other optimizations. Unlike + `storage.batch-loading` this option will not change the behavior of + the storage backend. + +- `setTimestamp(long)` - Sets the timestamp for this transaction as + communicated to the storage backend for persistence. Depending on + the storage backend, this setting may be ignored. For eventually + consistent backends, this is the timestamp used to resolve write + conflicts. If this setting is not explicitly specified, JanusGraph + uses the current time. + +- `setVertexCacheSize(long size)` - The number of vertices this + transaction caches in memory. The larger this number, the more + memory a transaction can potentially consume. If this number is too + small, a transaction might have to re-fetch data which causes delays + in particular for long running transactions. + +- `checkExternalVertexExistence(boolean)` - Whether this transaction + should verify the existence of vertices for user provided vertex + ids. Such checks requires access to the database which takes time. + The existence check should only be disabled if the user is + absolutely sure that the vertex must exist - otherwise data + corruption can ensue. + +- `checkInternalVertexExistence(boolean)` - Whether this transaction + should double-check the existence of vertices during query + execution. This can be useful to avoid **phantom vertices** on + eventually consistent storage backends. Disabled by default. + Enabling this setting can slow down query processing. + +- `consistencyChecks(boolean)` - Whether JanusGraph should enforce + schema level consistency constraints (e.g. multiplicity + constraints). Disabling consistency checks leads to better + performance but requires that the user ensures consistency + confirmation at the application level to avoid inconsistencies. USE + WITH GREAT CARE! + +Once, the desired configuration options have been specified, the new +transaction is started via `start()` which returns a +`JanusGraphTransaction`. diff --git a/docs/bdb.adoc b/docs/bdb.adoc deleted file mode 100644 index 0077575f3a..0000000000 --- a/docs/bdb.adoc +++ /dev/null @@ -1,44 +0,0 @@ -[[bdb]] -== Oracle Berkeley DB Java Edition - -[quote, 'http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html[Oracle Berkeley DB Java Edition Homepage]'] -Oracle Berkeley DB Java Edition is an open source, embeddable, transactional -storage engine written entirely in Java. It takes full advantage of the Java -environment to simplify development and deployment. The architecture of -Oracle Berkeley DB Java Edition supports very high performance and concurrency -for both read-intensive and write-intensive workloads. - -The http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html[Oracle Berkeley DB Java Edition] storage backend runs in the same JVM as JanusGraph and provides local persistence on a single machine. Hence, the BerkeleyDB storage backend requires that all of the graph data fits on the local disk and all of the frequently accessed graph elements fit into main memory. This imposes a practical limitation of graphs with 10-100s million vertices on commodity hardware. However, for graphs of that size the BerkeleyDB storage backend exhibits high performance because all data can be accessed locally within the same JVM. - -=== BerkeleyDB JE Setup - -Since BerkeleyDB runs in the same JVM as JanusGraph, connecting the two only requires a simple configuration and no additional setup: - -[source, java] -JanusGraph g = JanusGraphFactory.build(). -set("storage.backend", "berkeleyje"). -set("storage.directory", "/data/graph"). -open(); - -In the Gremlin Console, you can not define the type of the variables `conf` and `g`. Therefore, simply leave off the type declaration. - -=== BerkeleyDB Specific Configuration - -Refer to <> for a complete listing of all BerkeleyDB specific configuration options in addition to the general JanusGraph configuration options. - -When configuring BerkeleyDB it is recommended to consider the following BerkeleyDB specific configuration options: - -* *transactions*: Enables transactions and detects conflicting database operations. *CAUTION:* While disabling transactions can lead to better performance it can cause to inconsistencies and even corrupt the database if multiple JanusGraph instances interact with the same instance of BerkeleyDB. -* *cache-percentage*: The percentage of JVM heap space (configured via -Xmx) to be allocated to BerkeleyDB for its cache. Try to give BerkeleyDB as much space as possible without causing memory problems for JanusGraph. For instance, if JanusGraph only runs short transactions, use a value of 80 or higher. - -=== Ideal Use Case - -The BerkeleyDB storage backend is best suited for small to medium size graphs with up to 100 million vertices on commodity hardware. For graphs of that size, it will likely deliver higher performance than the distributed storage backends. Note, that BerkeleyDB is also limited in the number of concurrent requests it can handle efficiently because it runs on a single machine. Hence, it is not well suited for applications with many concurrent users mutating the graph, even if that graph is small to medium size. - -Since BerkeleyDB runs in the same JVM as JanusGraph, this storage backend is ideally suited for unit testing of application code using JanusGraph. - -=== Global Graph Operations - -JanusGraph backed by BerkeleyDB supports global graph operations such as iterating over all vertices or edges. However, note that such operations need to scan the entire database which can require a significant amount of time for larger graphs. - -In order to not run out of memory, it is advised to disable transactions (`storage.transactions=false`) when iterating over large graphs. Having transactions enabled requires BerkeleyDB to acquire read locks on the data it is reading. When iterating over the entire graph, these read locks can easily require more memory than is available. diff --git a/docs/bigtable.adoc b/docs/bigtable.adoc deleted file mode 100644 index 0cd8483df0..0000000000 --- a/docs/bigtable.adoc +++ /dev/null @@ -1,35 +0,0 @@ -[[bigtable]] -== Google Cloud Bigtable - -[.tss-center.tss-width-250] -image:Cloud-Bigtable.svg[link="https://cloud.google.com/bigtable/"] - -[quote, 'https://cloud.google.com/bigtable/[Google Cloud Bigtable Homepage]'] -____ -Cloud Bigtable is Google's NoSQL Big Data database service. It's the same database that powers many core Google -services, including Search, Analytics, Maps, and Gmail. - -Bigtable is designed to handle massive workloads at consistent low latency and high throughput, so it's a great choice -for both operational and analytical applications, including IoT, user analytics, and financial data analysis. -____ - -=== Bigtable Setup - -Bigtable implements the HBase interface for all data access operations, and -requires a few configuration options to connect. - -==== Connecting to Bigtable - -Configuring JanusGraph to connect to Bigtable is achieved by using the `hbase` backend, along with a custom connection -implementation, the project id of the Google Cloud Platform project containing the Bigtable instance, and the Cloud -Bigtable instance id you are connecting to. - -Example: - -[source,changelog] -.... -storage.backend=hbase -storage.hbase.ext.hbase.client.connection.impl=com.google.cloud.bigtable.hbase1_x.BigtableConnection -storage.hbase.ext.google.bigtable.project.id= -storage.hbase.ext.google.bigtable.instance.id= -.... diff --git a/docs/build-and-copy-docs.sh b/docs/build-and-copy-docs.sh deleted file mode 100755 index 1e488e44ee..0000000000 --- a/docs/build-and-copy-docs.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -u -# -# Copyright 2017 JanusGraph Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -################################################################################ - -# This script assumes the existence of two checked out Git repos: -# JanusGraph/janusgraph and JanusGraph/docs.janusgraph.org -# -# It will build the docs in the first repo and copy them to the second repo, such -# that a PR can be created in the second repo and pushed for review. Once the -# PR in docs.janusgraph.org is submitted, it is published to -# http://docs.janusgraph.org within minutes by GitHub since that domain is hosted -# by GitHub directly from the repo. - -################################################################################ - -# If your checked out JanusGraph Git repos under the same dir, e.g., -# ${GITHUB_ROOT}/janusgraph, then you can run this script as follows: -# -# env GITHUB_ROOT=[...] path/to/this/script.sh -# -# If on top of the above, your ${GITHUB_ROOT} == ${HOME}/github, you can run -# this script simply as-is: -# -# path/to/this/script.sh -# -# If needed, you can modify the variables below when running this script to -# customize your directories or options (such as documentation version to -# update) as follows: -# -# env VAR=[...] VAR2=[...] path/to/this/script.sh -# -declare -r GITHUB_ROOT="${GITHUB_ROOT:-${HOME}/github}" -declare -r SOURCE_REPO_DIR="${SOURCE_REPO_DIR:-${GITHUB_ROOT}/janusgraph/janusgraph}" -declare -r VERSION="$(xmllint --xpath '/*[local-name()="project"]/*[local-name()="version"]/text()' "${SOURCE_REPO_DIR}/pom.xml")" -declare -r DOCS_REPO_DIR="${DOCS_REPO_DIR:-${GITHUB_ROOT}/janusgraph/docs.janusgraph.org}" -declare -r DOCS_REPO_VERSION_DIR="${DOCS_REPO_DIR}/${VERSION}" - -if ! [ -d "${SOURCE_REPO_DIR}" ]; then - echo "Source repo dir: ${SOURCE_REPO_DIR} does not exist; exiting." >&2 - exit 1 -elif ! [ -d "${DOCS_REPO_VERSION_DIR}" ]; then - mkdir -p "${DOCS_REPO_VERSION_DIR}" -fi - -pushd "${SOURCE_REPO_DIR}" >& /dev/null 2>&1 - -echo "Running 'mvn clean' ..." -declare -r MVN_CLEAN_LOG="$(mktemp /tmp/janusgraph-doc.mvn-clean.XXXXXX)" -mvn clean -DskipTests=true -pl janusgraph-doc -am > "${MVN_CLEAN_LOG}" 2>&1 -if [ $? -ne 0 ]; then - echo "'mvn clean' failed; see ${MVN_CLEAN_LOG} for more info; exiting" >&2 - exit 2 -else - echo "'mvn clean' step succeeded; continuing." -fi - -echo "Running 'mvn install' ..." -declare -r MVN_INSTALL_LOG="$(mktemp /tmp/janusgraph-doc.mvn-install.XXXXXX)" -mvn install -DskipTests=true -pl janusgraph-doc -am > "${MVN_INSTALL_LOG}" 2>&1 -if [ $? -ne 0 ]; then - echo "'mvn install' failed; see ${MVN_INSTALL_LOG} for more info; exiting" >&2 - exit 2 -else - echo "'mvn install' step succeeded; continuing." -fi - -echo "Deleting all files in ${DOCS_REPO_VERSION_DIR}/* ..." -rm -rf "${DOCS_REPO_VERSION_DIR}"/* -echo "Copying generated docs to 'docs.janusgraph.org' repo ..." -cp -r janusgraph-doc/target/docs/chunk/* "${DOCS_REPO_VERSION_DIR}" -echo "Done." - -popd >& /dev/null 2>&1 diff --git a/docs/building.adoc b/docs/building.adoc deleted file mode 100644 index 7eac5481ae..0000000000 --- a/docs/building.adoc +++ /dev/null @@ -1,43 +0,0 @@ -[[building]] -== Building JanusGraph - -To build JanusGraph you need http://git-scm.com/[git] and http://maven.apache.org/[Maven]. - -. Clone the https://github.com/JanusGraph/janusgraph[JanusGraph repository from GitHub] to a local directory. -. In that directory, execute `mvn clean install`. This will build JanusGraph and run the internal test suite. The internal test suite has no external dependencies. Note, that running all test cases requires a significant amount of time. To skip the tests when building JanusGraph, execute `mvn clean install -DskipTests` -. For comprehensive test coverage, execute `mvn clean test -P comprehensive`. This will run additional test covering communication to external storage backends, performance tests and concurrency tests. The comprehensive test suite uses Cassandra and HBase as external databases and requires that Cassandra and HBase are installed. Note, that running the comprehensive test suite requires a significant amount of of time (> 1 hour). - -=== Depending on JanusGraph Snapshots - -For developing against the most current version of JanusGraph, depend on JanusGraph snapshot releases. Note, that these releases are development releases and therefore unstable and likely to change. Unless one is interested in the most recent development status of JanusGraph, we recommend to use the stable JanusGraph release instead. - -[source, xml] - - org.janusgraph - janusgraph-core - X.Y.Z-SNAPSHOT - - -Check the https://github.com/JanusGraph/janusgraph/tree/master[master branch] for the most current release version. -SNAPSHOTs will be available through the https://oss.sonatype.org/content/repositories/snapshots/org/janusgraph/[Sonatype repository]. - -When adding this dependency, be sure to add the following repository to the `pom.xml`: - -[source, xml] - - sonatype-nexus-snapshots - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots - - false - - - true - - - -=== FAQs - -*Maven build causes dozens of "[WARNING] We have a duplicate..." errors* - -Make sure to use the maven-assembly-plugin when building or depending on JanusGraph. diff --git a/docs/bulkloading.adoc b/docs/bulkloading.adoc deleted file mode 100644 index 46a9516ec4..0000000000 --- a/docs/bulkloading.adoc +++ /dev/null @@ -1,103 +0,0 @@ -[[bulk-loading]] -== Bulk Loading - -There are a number of configuration options and tools that make ingesting large amounts of graph data into JanusGraph more efficient. Such ingestion is referred to as _bulk loading_ in contrast to the default _transactional loading_ where small amounts of data are added through individual transactions. - -There are a number of use cases for bulk loading data into JanusGraph, including: - -* Introducing JanusGraph into an existing environment with existing data and migrating or duplicating this data into a new JanusGraph cluster. -* Using JanusGraph as an end point of an http://en.wikipedia.org/wiki/Extract,_transform,_load[ETL] process. -* Adding an existing or external graph datasets (e.g. publicly available http://linkeddata.org/[RDF datasets]) to a running JanusGraph cluster. -* Updating a JanusGraph graph with results from a graph analytics job. - -This page describes configuration options and tools that make bulk loading more efficient in JanusGraph. Please observe the limitations and assumptions for each option carefully before proceeding to avoid data loss or data corruption. - -This documentation focuses on JanusGraph specific optimization. In addition, consider improving the chosen storage backend and (optional) index backend for high write performance. Please refer to the documentation of the respective backend for more information. - -=== Configuration Options - -==== Batch Loading - -Enabling the `storage.batch-loading` configuration option will have the biggest positive impact on bulk loading times for most applications. Enabling batch loading disables JanusGraph internal consistency checks in a number of places. Most importantly, it disables locking. In other words, JanusGraph assumes that the data to be loaded into JanusGraph is consistent with the graph and hence disables its own checks in the interest of performance. - -In many bulk loading scenarios it is significantly cheaper to ensure data consistency prior to loading the data then ensuring data consistency while loading it into the database. The `storage.batch-loading` configuration option exists because of this observation. - -For example, consider the use case of bulk loading existing user profiles into JanusGraph. Furthermore, assume that the username property key has a unique composite index defined on it, i.e. usernames must be unique across the entire graph. If the user profiles are imported from another database, username uniqueness might already guaranteed. If not, it is simple to sort the profiles by name and filter out duplicates or writing a Hadoop job that does such filtering. Now, we can enable `storage.batch-loading` which significantly reduces the bulk loading time because JanusGraph does not have to check for every added user whether the name already exists in the database. - -*Important*: Enabling `storage.batch-loading` requires the user to ensure that the loaded data is internally consistent and consistent with any data already in the graph. In particular, concurrent type creation can lead to severe data integrity issues when batch loading is enabled. Hence, we *strongly* encourage disabling automatic type creation by setting `schema.default = none` in the graph configuration. - -==== Optimizing ID Allocation - -===== ID Block Size - -Each newly added vertex or edge is assigned a unique id. JanusGraph's id pool manager acquires ids in blocks for a particular JanusGraph instance. The id block acquisition process is expensive because it needs to guarantee globally unique assignment of blocks. Increasing `ids.block-size` reduces the number of acquisitions but potentially leaves many ids unassigned and hence wasted. For transactional workloads the default block size is reasonable, but during bulk loading vertices and edges are added much more frequently and in rapid succession. Hence, it is generally advisable to increase the block size by a factor of 10 or more depending on the number of vertices to be added per machine. - -*Rule of thumb*: Set `ids.block-size` to the number of vertices you expect to add per JanusGraph instance per hour. - -*Important:* All JanusGraph instances MUST be configured with the same value for `ids.block-size` to ensure proper id allocation. Hence, be careful to shut down all JanusGraph instances prior to changing this value. - -===== ID Acquisition Process - -When id blocks are frequently allocated by many JanusGraph instances in parallel, allocation conflicts between instances will inevitably arise and slow down the allocation process. In addition, the increased write load due to bulk loading may further slow down the process to the point where JanusGraph considers it failed and throws an exception. There are three configuration options that can be tuned to avoid this. - -1) `ids.authority.wait-time` configures the time in milliseconds the id pool manager waits for an id block application to be acknowledged by the storage backend. The shorter this time, the more likely it is that an application will fail on a congested storage cluster. - -*Rule of thumb*: Set this to the sum of the 95th percentile read and write times measured on the storage backend cluster under load. -*Important*: This value should be the same across all JanusGraph instances. - -2) `ids.renew-timeout` configures the number of milliseconds JanusGraph’s id pool manager will wait in total while attempting to acquire a new id block before failing. - -*Rule of thumb*: Set this value to be as large feasible to not have to wait too long for unrecoverable failures. The only downside of increasing it is that JanusGraph will try for a long time on an unavailable storage backend cluster. - -==== Optimizing Writes and Reads - -===== Buffer Size - -JanusGraph buffers writes and executes them in small batches to reduce the number of requests against the storage backend. The size of these batches is controlled by `storage.buffer-size`. When executing a lot of writes in a short period of time, it is possible that the storage backend can become overloaded with write requests. In that case, increasing `storage.buffer-size` can avoid failure by increasing the number of writes per request and thereby lowering the number of requests. - -However, increasing the buffer size increases the latency of the write request and its likelihood of failure. Hence, it is not advisable to increase this setting for transactional loads and one should carefully experiment with this setting during bulk loading. - -===== Read and Write Robustness - -During bulk loading, the load on the cluster typically increases making it more likely for read and write operations to fail (in particular if the buffer size is increased as described above). -`storage.read-attempts` and `storage.write-attempts` configure how many times JanusGraph will attempt to execute a read or write operation against the storage backend before giving up. If it is expected that there is a high load on the backend during bulk loading, it is generally advisable to increase these configuration options. - -`storage.attempt-wait` specifies the number of milliseconds that JanusGraph will wait before re-attempting a failed backend operation. A higher value can ensure that operation re-tries do not further increase the load on the backend. - -//=== Tools - -//==== JanusGraph-Hadoop - -//For very large graphs the best option to load data efficiently is <> using one of the supported input format and specifying JanusGraph as the output format. - -//==== BatchGraph - -//For medium size graph datasets (up to 100s million edges), TinkerPop ' http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/#_batchgraph[BatchGraph] is a useful tool for bulk loading data into JanusGraph from a single machine through JanusGraph's native Blueprints interface. BatchGraph effectively caches externally provided vertex ids to eliminate reads against JanusGraph. This allows bulk loading with minimal read load. - -//BatchGraph is limited to single machine bulk loading use cases and requires enough local RAM to hold the entire vertex id cache in memory. BatchGraph supports id compression to reduce the memory requirements. Please refer to the https://github.com/tinkerpop/blueprints/wiki/Batch-Implementation[BatchGraph documentation] for more information on how to use BatchGraph most effectively. - -=== Strategies - -==== Parallelizing the Load - -By parallelizing the bulk loading across multiple machines, the load time can be greatly reduced if JanusGraph's storage backend cluster is large enough to serve the additional requests. This is essentially the approach <> takes to bulk loading data into JanusGraph using MapReduce. - -If Hadoop cannot be used for parallelizing the bulk loading process, here are some high level guidelines for effectively parallelizing the loading process: - -* In some cases, the graph data can be decomposed into multiple disconnected subgraphs. Those subgraphs can be loaded independently in parallel across multiple machines (for instance, using BatchGraph as described above). -* If the graph cannot be decomposed, it is often beneficial to load in multiple steps where the last two steps can be parallelized across multiple machines: -.. Make sure the vertex and edge data sets are de-duplicated and consistent. -.. Set `batch-loading=true`. Possibly optimize additional configuration settings described above. -.. Add all the vertices with their properties to the graph (but no edges). Maintain a (distributed) map from vertex id (as defined by the loaded data) to JanusGraph's internal vertex id (i.e. `vertex.getId()`) which is a 64 bit long id. -.. Add all the edges using the map to look-up JanusGraph's vertex id and retrieving the vertices using that id. - - -// TODO: BatchGraph no longer exists, what should we point users to instead (and does sorting matter there)? -//==== Data Sorting -// -//Presorting the data to be bulk loaded can significantly increase the loading performance through BatchGraph. The http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/#_batchgraph[BatchGraph] documentation describes this strategy in more detail. It has been reported that loading times were decreased by a factor of 2 or more when presorting the bulk loaded data. - -=== Q&A - - * *What should I do to avoid the following exception during batch-loading:* `java.io.IOException: ID renewal thread on partition [X] did not complete in time.`? -This exception is mostly likely caused by repeated time-outs during the id allocation phase due to highly stressed storage backend. Refer to the section on _ID Allocation Optimization_ above. diff --git a/docs/static/images/cassandra-small.svg b/docs/cassandra-small.svg similarity index 100% rename from docs/static/images/cassandra-small.svg rename to docs/cassandra-small.svg diff --git a/docs/cassandra.adoc b/docs/cassandra.adoc deleted file mode 100644 index 0d77e6176a..0000000000 --- a/docs/cassandra.adoc +++ /dev/null @@ -1,232 +0,0 @@ -[[cassandra]] -== Apache Cassandra - -[quote, 'http://cassandra.apache.org/[Apache Cassandra Homepage]'] -The Apache Cassandra database is the right choice when you need -scalability and high availability without compromising -performance. Linear scalability and proven fault-tolerance on -commodity hardware or cloud infrastructure make it the perfect -platform for mission-critical data. Cassandra's support for -replicating across multiple datacenters is best-in-class, providing -lower latency for your users and the peace of mind of knowing that you -can survive regional outages. The largest known Cassandra cluster has -over 300 TB of data in over 400 machines. - -The following sections outline the various ways in which JanusGraph can be used in concert with Apache Cassandra. - -[[cassandra-storage-backend]] -=== Cassandra Storage Backend - -JanusGraph provides the following backends for use with Cassandra: - -* `cql` - CQL based driver. This is the recommended driver. -* `cassandrathrift` - JanusGraph's Thrift connection pool driver -* `cassandra` - https://github.com/Netflix/astyanax[Astyanax] driver. The Astyanax project is https://medium.com/netflix-techblog/astyanax-retiring-an-old-friend-6cca1de9ac4[retired]. -* `embeddedcassandra` - Embedded driver for running Cassandra and JanusGraph within the same JVM - -Cassandra has two protocols for clients to use: CQL and Thrift. Thrift was the original interface, however it was deprecated starting with Cassandra 2.1. The core of JanusGraph was originally written before the deprecation of Thrift, and it has several classes that support Thrift. With Cassandra 4.0, Thrift support will be removed in Cassandra. JanusGraph users are recommended to use the `cql` storage backend. - -[NOTE] -If you plan to use a Thrift-based driver and you are using Cassandra 2.2 or higher, you need to explicitly enable Thrift so that JanusGraph can connect to the cluster. -Do so by running `./bin/nodetool enablethrift` on every Cassandra node. - -[NOTE] -If security is enabled on Cassandra, the user must have `CREATE permission on `, -otherwise the keyspace must be created ahead of time by an administrator including the required tables or the user must have `CREATE permission on `. -The create table file containing the required tables is located in `conf/cassandra/cassandraTables.cql`. Please define your keyspace before executing it. - - -[[cassandra-local-server-mode]] -=== Local Server Mode - -image:modes-local.png[] - -Cassandra can be run as a standalone database on the same local host as JanusGraph and the end-user application. In this model, JanusGraph and Cassandra communicate with one another via a `localhost` socket. Running JanusGraph over Cassandra requires the following setup steps: - -. http://cassandra.apache.org/download/[Download Cassandra], unpack it, and set filesystem paths in `conf/cassandra.yaml` and `conf/log4j-server.properties` -. Connecting Gremlin Server to Cassandra using the default configuration files provided in the pre-packaged distribution requires that Cassandra Thrift is enabled. To enable Cassandra Thrift open `conf/cassandra.yaml` and update `start_rpc: false` to `start_rpc: true`. If Cassandra is already running Thrift can be started manually with `bin/nodetool enablethrift`. the Thrift status can be verified with `bin/nodetool` statusthrift. -. Start Cassandra by invoking `bin/cassandra -f` on the command line in the directory where Cassandra was unpacked. Read output to check that Cassandra started successfully. - -Now, you can create a Cassandra JanusGraph as follows:: - -[source, java] -JanusGraph g = JanusGraphFactory.build(). -set("storage.backend", "cql"). -set("storage.hostname", "127.0.0.1"). -open(); - -In the Gremlin Console, you can not define the type of the variables `conf` and `g`. Therefore, simply leave off the type declaration. - -=== Local Container Mode - -Cassandra does not have a native install for Windows or OSX. One of the easiest ways to run Cassandra on OSX, Windows, or Linux is to use a Docker Container. You can download and run Cassandra with a single https://www.docker.com/[Docker] command. It is important to install a version that is supported by the version of JanusGraph you intend to use. The compatible versions can be found under the Tested Compatibility section of the specific release on the https://github.com/JanusGraph/janusgraph/releases[Releases page]. The https://hub.docker.com/_/cassandra/[Cassandra Docker Hub page] can be referenced for the available versions and useful commands. In the command below an environment variable is being set to enable Cassandra Thrift with `-e CASSANDRA_START_RPC=true`. A description of the ports can be found https://docs.datastax.com/en/cassandra/latest/cassandra/configuration/secureFireWall.html[here]. Port 9160 is used for the Thrift client API. Port 9042 is for CQL native clients. Ports 7000, 7001 and 7099 are for inter-node communication. Version 3.11 of Cassandra was the latest compatible version for JanusGraph 0.2.0 and is specified in the reference command below. - -[source, bash] -docker run --name jg-cassandra -d -e CASSANDRA_START_RPC=true -p 9160:9160 \ --p 9042:9042 -p 7199:7199 -p 7001:7001 -p 7000:7000 cassandra:3.11 - -=== Remote Server Mode - -image:modes-distributed.png[] - -When the graph needs to scale beyond the confines of a single machine, then Cassandra and JanusGraph are logically separated into different machines. In this model, the Cassandra cluster maintains the graph representation and any number of JanusGraph instances maintain socket-based read/write access to the Cassandra cluster. The end-user application can directly interact with JanusGraph within the same JVM as JanusGraph. - -For example, suppose we have a running Cassandra cluster where one of the machines has the IP address 77.77.77.77, then connecting JanusGraph with the cluster is accomplished as follows (comma separate IP addresses to reference more than one machine): - -[source, java] -JanusGraph graph = JanusGraphFactory.build(). -set("storage.backend", "cql"). -set("storage.hostname", "77.77.77.77"). -open(); - -In the Gremlin Console, you can not define the type of the variables `conf` and `g`. Therefore, simply leave off the type declaration. - -=== Remote Server Mode with Gremlin Server - -image:modes-rexster.png[] - -Gremlin Server can be wrapped around each JanusGraph instance defined in the previous subsection. In this way, the end-user application need not be a Java-based application as it can communicate with Gremlin Server as a client. This type of deployment is great for polyglot architectures where various components written in different languages need to reference and compute on the graph. - -Start Gremlin Server using `bin/gremlin-server.sh` and then in an external Gremlin Console session using `bin/gremlin.sh` you can send Gremlin commands over the wire: - ----- -:plugin use tinkerpop.server -:remote connect tinkerpop.server conf/remote.yaml -:> g.addV() ----- - -In this case, each Gremlin Server would be configured to connect to the Cassandra cluster. The following shows the graph specific fragment of the Gremlin Server configuration. Refer to <> for a complete example and more information on how to configure the server. - -[source, yaml] ----- -... -graphs: { - g: conf/janusgraph-cql.properties} -plugins: - - janusgraph.imports -... ----- -For more information about Gremlin Server see the http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference#gremlin-server[Apache TinkerPop documentation] - -=== JanusGraph Embedded Mode - -image:modes-embedded.png[] - -Finally, Cassandra can be embedded in JanusGraph, which means, that JanusGraph and Cassandra run in the same JVM and communicate via in process calls rather than over the network. This removes the (de)serialization and network protocol overhead and can therefore lead to considerable performance improvements. In this deployment mode, JanusGraph internally starts a cassandra daemon and JanusGraph no longer connects to an existing cluster but is its own cluster. - -To use JanusGraph in embedded mode, simply configure `embeddedcassandra` as the storage backend. The configuration options listed below also apply to embedded Cassandra. In creating a JanusGraph cluster, ensure that the individual nodes can discover each other via the Gossip protocol, so setup a JanusGraph-with-Cassandra-embedded cluster much like you would a stand alone Cassandra cluster. When running JanusGraph in embedded mode, the Cassandra yaml file is configured using the additional configuration option `storage.conf-file`, which specifies the yaml file as a full url, e.g. `storage.conf-file = file:///home/cassandra.yaml`. - -When running a cluster with JanusGraph and Cassandra embedded, it is advisable to expose JanusGraph through the Gremlin Server so that applications can remotely connect to the JanusGraph graph database and execute queries. - -Note, that running JanusGraph with Cassandra embedded requires GC tuning. While embedded Cassandra can provide lower latency query answering, its GC behavior under load is less predictable. - -=== Cassandra Specific Configuration - -Refer to <> for a complete listing of all Cassandra specific configuration options in addition to the general JanusGraph configuration options. - -When configuring Cassandra it is recommended to consider the following Cassandra specific configuration options: - -* *read-consistency-level*: Cassandra consistency level for read operations -* *write-consistency-level*: Cassandra consistency level for write operations -* *replication-factor*: The replication factor to use. The higher the replication factor, the more robust the graph database is to machine failure at the expense of data duplication. *The default value should be overwritten for production system to ensure robustness. A value of 3 is recommended.* This replication factor can only be set when the keyspace is initially created. **On an existing keyspace, this value is ignored.** -* *thrift.frame_size_mb*: The maximum frame size to be used by thrift for transport. Increase this value when retrieving very large result sets. **Only applicable when storage.backend=cassandrathrift** -* *keyspace*: The name of the keyspace to store the JanusGraph graph in. Allows multiple JanusGraph graphs to co-exist in the same Cassandra cluster. - -For more information on Cassandra consistency levels and acceptable values, please refer to the http://wiki.apache.org/cassandra/API10[Cassandra Thrift API]. In general, higher levels are more consistent and robust but have higher latency. - -=== Global Graph Operations - -JanusGraph over Cassandra supports global vertex and edge iteration. However, note that all these vertices and/or edges will be loaded into memory which can cause `OutOfMemoryException`. Use <> to iterate over all vertices or edges in large graphs effectively. - -=== Deploying on Amazon EC2 - -[quote, 'http://aws.amazon.com/ec2/[Amazon EC2]'] -Amazon Elastic Compute Cloud (Amazon EC2) is a web service that provides resizable compute capacity in the cloud. It is designed to make web-scale computing easier for developers. - -Follow these steps to setup a Cassandra cluster on EC2 and deploy JanusGraph over Cassandra. To follow these instructions, you need an Amazon AWS account with established authentication credentials and some basic knowledge of AWS and EC2. - -==== Setup Cassandra Cluster - -These instructions for configuring and launching the DataStax Cassandra Community Edition AMI are based on the DataStax AMI Docs and focus on aspects relevant for a JanusGraph deployment. - -==== Setting up Security Group - -* Navigate to the EC2 Console Dashboard, then click on "Security Groups" under "Network & Security". - -* Create a new security group. Click Inbound. Set the "Create a new rule" dropdown menu to "Custom TCP rule". Add a rule for port 22 from source 0.0.0.0/0. Add a rule for ports 1024-65535 from the security group members. If you don't want to open all unprivileged ports among security group members, then at least open 7000, 7199, and 9160 among security group members. Tip: the "Source" dropdown will autocomplete security group identifiers once "sg" is typed in the box, so you needn't have the exact value ready beforehand. - -==== Launch DataStax Cassandra AMI - -* "Launch the https://aws.amazon.com/amis/datastax-auto-clustering-ami-2-2[DataStax AMI] in your desired zone - -* On the Instance Details page of the Request Instances Wizard, set "Number of Instances" to your desired number of Cassandra nodes. Set "Instance Type" to at least m1.large. We recommend m1.large. - -* On the Advanced Instance Options page of the Request Instances Wizard, set the "as text" radio button under "User Data", then fill this into the text box: - ----- ---clustername [cassandra-cluster-name] ---totalnodes [number-of-instances] ---version community ---opscenter no ----- - -[number-of-instances] in this configuration must match the number of EC2 instances configured on the previous wizard page. [cassandra-cluster-name] can be any string used for identification. For example: - ----- ---clustername janusgraph ---totalnodes 4 ---version community ---opscenter no ----- - -* On the Tags page of the Request Instances Wizard you can apply any desired configurations. These tags exist only at the EC2 administrative level and have no effect on the Cassandra daemons' configuration or operation. - -* On the Create Key Pair page of the Request Instances Wizard, either select an existing key pair or create a new one. The PEM file containing the private half of the selected key pair will be required to connect to these instances. - -* On the Configure Firewall page of the Request Instances Wizard, select the security group created earlier. - -* Review and launch instances on the final wizard page. - -==== Verify Successful Instance Launch - -* SSH into any Cassandra instance node: `ssh -i [your-private-key].pem ubuntu@[public-dns-name-of-any-cassandra-instance]` - -* Run the Cassandra nodetool `nodetool -h 127.0.0.1 ring` to inspect the state of the Cassandra token ring. You should see as many nodes in this command's output as instances launched in the previous steps. - -Note, that the AMI takes a few minutes to configure each instance. A shell prompt will appear upon successful configuration when you SSH into the instance. - -==== Launch JanusGraph Instances - -Launch additional EC2 instances to run JanusGraph which are either configured in Remote Server Mode or Remote Server Mode with Gremlin-Server as described above. You only need to note the IP address of one of the Cassandra cluster instances and configure it as the host name. The particular EC2 instance to run and the particular configuration depends on your use case. - -==== Example JanusGraph Instance on Amazon Linux AMI - -* Launch the http://aws.amazon.com/amazon-linux-ami[Amazon Linux AMI] in the same zone of the Cassandra cluster. Choose your desired EC2 instance type depending on the amount of resources you need. Use the default configuration options and select the same Key Pair and Security Group as for the Cassandra cluster configured in the previous step. - -* SSH into the newly created instance via `ssh -i [your-private-key].pem ec2-user@[public-dns-name-of-the-instance]`. You may have to wait a little for the instance to launch. - -* https://github.com/JanusGraph/janusgraph/releases[Download] the current JanusGraph distribution with `wget` and unpack the archive locally to the home directory. Start the Gremlin Console to verify that JanusGraph runs successfully. For more information on how to unpack JanusGraph and start the Gremlin Console, please refer to the <> - -* Create a configuration file with `vi janusgraph.properties` and add the following lines:: - - storage.backend = cql - storage.hostname = [IP-address-of-one-Cassandra-EC2-instance] - -You may add additional configuration options found on this page or in <>. - -* Start the Gremlin Console again and type the following:: - - gremlin> graph = JanusGraphFactory.open('janusgraph.properties') - ==>janusgraph[cql:[IP-address-of-one-Cassandra-EC2-instance]] - -You have successfully connected this JanusGraph instance to the Cassandra cluster and can start to operate on the graph. - -==== Connect to Cassandra cluster in EC2 from outside EC2 - -Opening the usual Cassandra ports (9160, 7000, 7199) in the security group is not enough, because the Cassandra nodes by default broadcast their ec2-internal IPs, and not their public-facing IPs. - -The resulting behavior is that you can open a JanusGraph graph on the cluster by connecting to port 9160 on any Cassandra node, but all requests to that graph time out. This is because Cassandra is telling the client to connect to an unreachable IP. - -To fix this, set the "broadcast-address" property for each instance in /etc/cassandra/cassandra.yaml to its public-facing IP, and restart the instance. Do this for all nodes in the cluster. Once the cluster comes back, nodetool reports the correct public-facing IPs to which connections from the local machine are allowed. - -Changing the "broadcast-address" property allows you to connect to the cluster from outside ec2, but it might also mean that traffic originating within ec2 will have to round-trip to the internet and back before it gets to the cluster. So, this approach is only useful for development and testing. diff --git a/docs/changelog.adoc b/docs/changelog.adoc deleted file mode 100644 index 8b89574521..0000000000 --- a/docs/changelog.adoc +++ /dev/null @@ -1,164 +0,0 @@ -[[changelog]] -[appendix] -== Release Notes - -=== Version 0.2.3 (Release Date: May 21, 2019) - -[source, xml] - - org.janusgraph - janusgraph-core - 0.2.3 - - -*Tested Compatibility:* - -* Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 -* Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 -* Google Bigtable 1.0.0 -* Oracle BerkeleyJE 7.3.7 -* Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 -* Apache Lucene 7.0.0 -* Apache Solr 5.5.4, 6.6.1, 7.0.0 -* Apache TinkerPop 3.2.9 -* Java 1.8 - -For more information on features and bug fixes in 0.2.3, see the GitHub milestone: - -* https://github.com/JanusGraph/janusgraph/milestone/9?closed=1 - -=== Version 0.2.2 (Release Date: October 9, 2018) - -[source, xml] - - org.janusgraph - janusgraph-core - 0.2.2 - - -*Tested Compatibility:* - -* Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 -* Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 -* Google Bigtable 1.0.0 -* Oracle BerkeleyJE 7.3.7 -* Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 -* Apache Lucene 7.0.0 -* Apache Solr 5.5.4, 6.6.1, 7.0.0 -* Apache TinkerPop 3.2.9 -* Java 1.8 - -For more information on features and bug fixes in 0.2.2, see the GitHub milestone: - -* https://github.com/JanusGraph/janusgraph/milestone/6?closed=1 - -=== Version 0.2.1 (Release Date: July 9, 2018) - -[source, xml] - - org.janusgraph - janusgraph-core - 0.2.1 - - -*Tested Compatibility:* - -* Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 -* Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 -* Google Bigtable 1.0.0 -* Oracle BerkeleyJE 7.3.7 -* Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 -* Apache Lucene 7.0.0 -* Apache Solr 5.5.4, 6.6.1, 7.0.0 -* Apache TinkerPop 3.2.9 -* Java 1.8 - -For more information on features and bug fixes in 0.2.1, see the GitHub milestone: - -* https://github.com/JanusGraph/janusgraph/milestone/5?closed=1 - -=== Version 0.2.0 (Release Date: October 11, 2017) - -[source, xml] - - org.janusgraph - janusgraph-core - 0.2.0 - - -*Tested Compatibility:* - -* Apache Cassandra 2.1.18, 2.2.10, 3.0.14, 3.11.0 -* Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 -* Google Bigtable 1.0.0-pre3 -* Oracle BerkeleyJE 7.3.7 -* Elasticsearch 1.7.6, 2.4.6, 5.6.2, 6.0.0-rc1 -* Apache Lucene 7.0.0 -* Apache Solr 5.5.4, 6.6.1, 7.0.0 -* Apache TinkerPop 3.2.6 -* Java 1.8 - -For more information on features and bug fixes in 0.2.0, see the GitHub milestone: - -* https://github.com/JanusGraph/janusgraph/milestone/2?closed=1 - -=== Version 0.1.1 (Release Date: May 11, 2017) - -[source, xml] - - org.janusgraph - janusgraph-core - 0.1.1 - - -*Tested Compatibility:* - -* Apache Cassandra 2.1.9 -* Apache HBase 0.98.8-hadoop2, 1.0.3, 1.1.8, 1.2.4 -* Google Bigtable 0.9.5.1 -* Oracle BerkeleyJE 7.3.7 -* Elasticsearch 1.5.1 -* Apache Lucene 4.10.4 -* Apache Solr 5.2.1 -* Apache TinkerPop 3.2.3 -* Java 1.8 - -For more information on features and bug fixes in 0.1.1, see the GitHub milestone: - -* https://github.com/JanusGraph/janusgraph/milestone/3?closed=1 - -=== Version 0.1.0 (Release Date: April 11, 2017) - -[source, xml] - - org.janusgraph - janusgraph-core - 0.1.0 - - -*Tested Compatibility:* - -* Apache Cassandra 2.1.9 -* Apache HBase 0.98.8-hadoop2, 1.0.3, 1.1.8, 1.2.4 -* Google Bigtable 0.9.5.1 -* Oracle BerkeleyJE 7.3.7 -* Elasticsearch 1.5.1 -* Apache Lucene 4.10.4 -* Apache Solr 5.2.1 -* Apache TinkerPop 3.2.3 -* Java 1.8 - -*Features added since version Titan 1.0.0:* - -* TinkerPop 3.2.3 compatibility -** Includes update to Spark 1.6.1 -* Query optimizations: JanusGraphStep folds in HasId and HasContainers can be folded in even mid-traversal -* Support Google Cloud Bigtable as a backend over the HBase interface -* Compatibility with newer versions of backend and index stores -** HBase 1.2 -** BerkeleyJE 7.3.7 -* Includes a number of bug fixes and optimizations - -For more information on features and bug fixes in 0.1.0, see the GitHub milestone: - -* https://github.com/JanusGraph/janusgraph/milestone/1?closed=1 diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000000..9ed207cfef --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,369 @@ +# Changelog + +## Version Compatibility + +The JanusGraph project is growing along with the rest of the graph and +big data ecosystem and utilized storage and indexing backends. Below are +version compatibilities between the various versions of components. For +dependent backend systems, different minor versions are typically +supported as well. It is strongly encouraged to verify version +compatibility prior to deploying JanusGraph. + +Although JanusGraph may be compatible with older and no longer supported +versions of its dependencies, users are warned that there are possible +risks and security exposures with running software that is no longer +supported or updated. Please check with the software providers to +understand their supported versions. Users are strongly encouraged to +use the latest versions of the software. + +### Version Compatibility Matrix +| JanusGraph | Storage Version | Cassandra | HBase | Bigtable | Elasticsearch | Solr | TinkerPop | Spark | Scala | +| ----- | ---- | ---- | ---- | ---- | ---- | ---- | --- | ---- | ---- | +| 0.1.z| 1| 1.2.z, 2.0.z, 2.1.z| 0.98.z, 1.0.z, 1.1.z, 1.2.z| 0.9.z, 1.0.0-preZ, 1.0.0| 1.5.z| 5.2.z| 3.2.z| 1.6.z| 2.10.z| +| 0.2.z | 1 | 1.2.z, 2.0.z, 2.1.z, 2.2.z, 3.0.z, 3.11.z | 0.98.z, 1.0.z, 1.1.z, 1.2.z, 1.3.z | 0.9.z, 1.0.0-preZ, 1.0.0 | 1.5-1.7.z, 2.3-2.4.z, 5.y, 6.y | 5.2-5.5.z, 6.2-6.6.z, 7.y | 3.2.z | 1.6.z | 2.10.z | + +## Release Notes +### Version 0.2.3 (Release Date: May 21, 2019) +Documentation: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.2.2 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.2.2" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 +- Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 +- Google Bigtable 1.0.0 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 +- Apache Lucene 7.0.0 +- Apache Solr 5.5.4, 6.6.1, 7.0.0 +- Apache TinkerPop 3.2.9 +- Java 1.8 + +For more information on features and bug fixes in 0.2.3, see the GitHub +milestone: + +- https://github.com/JanusGraph/janusgraph/milestone/9?closed=1 + +### Version 0.2.2 (Release Date: October 9, 2018) +Documentation: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.2.2 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.2.2" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 +- Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 +- Google Bigtable 1.0.0 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 +- Apache Lucene 7.0.0 +- Apache Solr 5.5.4, 6.6.1, 7.0.0 +- Apache TinkerPop 3.2.9 +- Java 1.8 + +For more information on features and bug fixes in 0.2.2, see the GitHub +milestone: + +- + +### Version 0.2.1 (Release Date: July 9, 2018) +Documentation: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.2.1 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.2.1" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 +- Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 +- Google Bigtable 1.0.0 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 +- Apache Lucene 7.0.0 +- Apache Solr 5.5.4, 6.6.1, 7.0.0 +- Apache TinkerPop 3.2.9 +- Java 1.8 + +For more information on features and bug fixes in 0.2.1, see the GitHub +milestone: + +- + +#### Upgrade Instructions +##### HBase TTL + +In JanusGraph 0.2.0, time-to-live (TTL) support was added for HBase +storage backend. In order to utilize the TTL capability on HBase, the +graph timestamps need to be MILLI. If the `graph.timestamps` property is +not explicitly set to MILLI, the default is MICRO in JanusGraph 0.2.0, +which does not work for HBase TTL. Since the `graph.timestamps` property +is FIXED, a new graph needs to be created to make any change of the +`graph.timestamps` property effective. + +### Version 0.2.0 (Release Date: October 11, 2017) +Documentation: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.2.0 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.2.0" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.18, 2.2.10, 3.0.14, 3.11.0 +- Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 +- Google Bigtable 1.0.0-pre3 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.7.6, 2.4.6, 5.6.2, 6.0.0-rc1 +- Apache Lucene 7.0.0 +- Apache Solr 5.5.4, 6.6.1, 7.0.0 +- Apache TinkerPop 3.2.6 +- Java 1.8 + +For more information on features and bug fixes in 0.2.0, see the GitHub +milestone: + +- + +#### Upgrade Instructions + +##### Elasticsearch + +JanusGraph 0.1.z is compatible with Elasticsearch 1.5.z. There were +several configuration options available, including transport client, +node client, and legacy configuration track. JanusGraph 0.2.0 is +compatible with Elasticsearch versions from 1.y through 6.y, however it +offers only a single configuration option using the REST client. + +##### Transport client + +The `TRANSPORT_CLIENT` interface has been replaced with `REST_CLIENT`. +When migrating an existing graph to JanusGraph 0.2.0, the `interface` +property must be set when connecting to the graph: +```conf +index.search.backend=elasticsearch +index.search.elasticsearch.interface=REST_CLIENT +index.search.hostname=127.0.0.1 +``` + +After connecting to the graph, the property update can be made permanent +by making the change with `JanusGraphManagement`: +```groovy +mgmt = graph.openManagement() +mgmt.set("index.search.elasticsearch.interface", "REST_CLIENT") +mgmt.commit() +``` + +##### Node client + +A node client with JanusGraph can be configured in a few ways. If the +node client was configured as a client-only or non-data node, follow the +steps from the [transport client](#_transport_client) section to connect +to the existing cluster using the `REST_CLIENT` instead. If the node +client was a data node (local-mode), then convert it into a standalone +Elasticsearch node, running in a separate JVM from your application +process. This can be done by using the node’s configuration from the +JanusGraph configuration to start a standalone Elasticsearch 1.5.z node. +For example, we start with these JanusGraph 0.1.z properties: + + index.search.backend=elasticsearch + index.search.elasticsearch.interface=NODE + index.search.conf-file=es-client.yml + index.search.elasticsearch.ext.node.name=alice + +where the configuration file `es-client.yml` has properties: + + node.data: true + path.data: /var/lib/elasticsearch/data + path.work: /var/lib/elasticsearch/work + path.logs: /var/log/elasticsearch + +The properties found in the configuration file `es-client.yml` and the +`index.search.elasticsearch.ext.*` properties can be inserted into +`$ES_HOME/config/elasticsearch.yml` so that a standalone Elasticsearch +1.5.z node can be started with the same properties. Keep in mind that if +any `path` locations have relative paths, those values may need to be +updated appropriately. Once the standalone Elasticsearch node is +started, follow the directions in the [transport +client](#_transport_client) section to complete the migration to the +`REST_CLIENT` interface. Note that the `index.search.conf-file` and +`index.search.elasticsearch.ext.*` properties are not used by the +`REST_CLIENT` interface, so they can be removed from the configuration +properties. + +##### Legacy configuration + +The legacy configuration track was not recommended in JanusGraph 0.1.z +and is no longer supported in JanusGraph 0.2.0. Users should refer to +the previous sections and migrate to the `REST_CLIENT`. + + +### Version 0.1.1 (Release Date: May 11, 2017) +Documentation: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.1.1 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.1.1" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.9 +- Apache HBase 0.98.8-hadoop2, 1.0.3, 1.1.8, 1.2.4 +- Google Bigtable 0.9.5.1 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.5.1 +- Apache Lucene 4.10.4 +- Apache Solr 5.2.1 +- Apache TinkerPop 3.2.3 +- Java 1.8 + +For more information on features and bug fixes in 0.1.1, see the GitHub +milestone: + +- + +### Version 0.1.0 (Release Date: April 11, 2017) +Documentation: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.1.0 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.1.0" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.9 +- Apache HBase 0.98.8-hadoop2, 1.0.3, 1.1.8, 1.2.4 +- Google Bigtable 0.9.5.1 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.5.1 +- Apache Lucene 4.10.4 +- Apache Solr 5.2.1 +- Apache TinkerPop 3.2.3 +- Java 1.8 + +**Features added since version Titan 1.0.0:** + +- TinkerPop 3.2.3 compatibility + + - Includes update to Spark 1.6.1 + +- Query optimizations: JanusGraphStep folds in HasId and HasContainers + can be folded in even mid-traversal + +- Support Google Cloud Bigtable as a backend over the HBase interface + +- Compatibility with newer versions of backend and index stores + + - HBase 1.2 + + - BerkeleyJE 7.3.7 + +- Includes a number of bug fixes and optimizations + +For more information on features and bug fixes in 0.1.0, see the GitHub +milestone: + +- + +#### Upgrade Instructions + +JanusGraph is based on the latest commit to the `titan11` branch of +[Titan repo](https://github.com/thinkaurelius/titan). + +JanusGraph has made the following changes to Titan, so you will need to +adjust your code and configuration accordingly: + +1. module names: `titan-*` are now `janusgraph-*` + +2. package names: `com.thinkaurelius.titan` are now `org.janusgraph` + +3. class names: `Titan*` are now `JanusGraph*` except in cases where + this would duplicate a word, e.g., `TitanGraph` is simply + `JanusGraph` rather than `JanusGraphGraph` + +For more information on how to configure JanusGraph to read data which +had previously been written by Titan refer to [Migration from titan](advanced-topics/migrating.md). + +### Unreleased Version 0.2.3 + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.2.3 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.2.3" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.1.20, 2.2.10, 3.0.14, 3.11.0 +- Apache HBase 0.98.24-hadoop2, 1.2.6, 1.3.1 +- Google Bigtable 1.0.0 +- Oracle BerkeleyJE 7.3.7 +- Elasticsearch 1.7.6, 2.4.6, 5.6.5, 6.0.1 +- Apache Lucene 7.0.0 +- Apache Solr 5.5.4, 6.6.1, 7.0.0 +- Apache TinkerPop 3.2.9 +- Java 1.8 + +For more information on features and bug fixes in 0.2.3, see the GitHub +milestone: + +- \ No newline at end of file diff --git a/docs/configref.adoc b/docs/configref.adoc deleted file mode 100644 index bc0ff09ea7..0000000000 --- a/docs/configref.adoc +++ /dev/null @@ -1,38 +0,0 @@ -[[config-ref]] -== Configuration Reference - -This section is the authoritative reference for JanusGraph configuration options. It includes all options for storage and indexing backends that are part of the official JanusGraph distribution. - -The table is automatically generated by traversing the keys and namespaces in JanusGraph's internal configuration management API. Hence, the configuration options as listed on this page are synchronized with a particular JanusGraph release. If a reference to a configuration option in other parts of this documentation is in conflict with its representation on this page, assume the version listed here to be correct. - -[[cfg-mutability]] -=== Mutability Levels - -Each configuration option has a certain mutability level that governs whether and how it can be modified after the database is opened for the first time. The following listing describes the mutability levels. - -FIXED:: -Once the database has been opened, these configuration options cannot be changed for the entire life of the database -GLOBAL_OFFLINE:: -These options can only be changed for the entire database cluster at once when all instances are shut down -GLOBAL:: -These options can only be changed globally across the entire database cluster -MASKABLE:: -These options are global but can be overwritten by a local configuration file -LOCAL:: -These options can only be provided through a local configuration file - -Refer to <> for information on how to change non-local configuration options. - -[[cfg-umbrella-ns]] -=== Umbrella Namespace - -Namespaces marked with an asterisk are *umbrella namespaces* which means that they can accommodate an arbitrary number of sub-namespaces - each of which uniquely identified by its name. The configuration options listed under an umbrella namespace apply only to those sub-namespaces. Umbrella namespaces are used to configure multiple system components that are of the same type and hence have the same configuration options. - -For example, the `log` namespace is an umbrella namespace because JanusGraph can interface with multiple logging backends, such as the `user` log, each of which has the same core set of configuration options. To configure the send batch size of the `user` log to 100 transaction changes, one would have to set the following option in the configuration - -[source, text] -log.user.send-batch-size = 100 - -=== Configuration Namespaces and Options - -include::listings/janusgraph_cfg.adoc[] diff --git a/docs/connecting-via-dotnet.adoc b/docs/connecting-via-dotnet.adoc deleted file mode 100644 index 2b3a05700e..0000000000 --- a/docs/connecting-via-dotnet.adoc +++ /dev/null @@ -1,51 +0,0 @@ -[[connecting-via-dotnet]] -== Connecting from .NET - -JanusGraph can be queried from a .NET application with Apache TinkerPop's http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#gremlin-DotNet[Gremlin.Net]. - -Gremlin traversals can be constructed with Gremlin.Net just like in Gremlin-Java or Gremiln-Groovy. -Refer to <> for an introduction to Gremlin and pointers to further resources. -The main syntactical difference for Gremlin.Net is that it follows .NET naming conventions, e.g., method names use PascalCase instead of camelCase. - -=== Getting Started with JanusGraph and Gremlin.Net - -To get started with Gremlin.Net: - -. Create a console application: -[source, bash] -dotnet new console -o GremlinExample - -. Add Gremlin.Net: -[source, bash] -dotnet add package Gremlin.Net -v $MAVEN{tinkerpop.version} - -. Create a `GraphTraversalSource` which is the basis for all Gremlin traversals: -+ -[source, csharp] ----- -var graph = new Graph(); -var client = new GremlinClient(new GremlinServer("localhost", 8182)); -// The client should be disposed on shut down to release resources -// and to close open connections with client.Dispose() -var g = graph.Traversal().WithRemote(new DriverRemoteConnection(client)); -// Reuse 'g' across the application ----- - -. Execute a simple traversal: -+ -[source, csharp] ----- -var herculesAge = g.V().Has("name", "hercules").Values("age").Next(); -Console.WriteLine($"Hercules is {herculesAge} years old."); ----- -+ -`Next()` is a terminal step that submits the traversal to the Gremlin Server and returns a single result. Other terminal steps can be found http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#_remoteconnection_submission_2[in TinkerPop's reference documentation]. -+ -The traversal can also be executed asynchronously by using `Promise()` which is the recommended way as the underlying driver in Gremlin.Net also works asynchronously: -+ -[source, csharp] -var herculesAge = await g.V().Has("name", "hercules").Values("age").Promise(t => t.Next()); - -=== JanusGraph Specific Types and Predicates - -JanusGraph contains some types and <> that are not part of Apache TinkerPop and are therefore also not supported by Gremlin.Net. diff --git a/docs/connecting-via-java.adoc b/docs/connecting-via-java.adoc deleted file mode 100644 index bc7baf8bf4..0000000000 --- a/docs/connecting-via-java.adoc +++ /dev/null @@ -1,88 +0,0 @@ -[[connecting-via-java]] -== Connecting from Java - -JanusGraph can be queried from a Java application with Apache TinkerPop's http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#connecting-via-java[Gremlin Driver]. -While it is possible to embed JanusGraph as a library inside a Java application and then directly connect to the backend, this section assumes that the application connects to JanusGraph Server. -For information on how to embed JanusGraph, see the https://github.com/JanusGraph/janusgraph/tree/master/janusgraph-examples[JanusGraph Examples projects]. - -This section only covers how applications can connect to JanusGraph Server. -Refer to <> for an introduction to Gremlin and pointers to further resources. - -=== Getting Started with JanusGraph and Gremlin-Java - -To get started with JanusGraph in Java: - -. Create an application with Maven: -+ -[source, bash] ----- -mvn archetype:generate -DgroupId=com.mycompany.project - -DartifactId=gremlin-example - -DarchetypeArtifactId=maven-archetype-quickstart - -DinteractiveMode=false ----- - -. Add dependencies on `janusgraph-core` and `gremlin-driver` to the `pom.xml` file: -+ -[source, xml] ----- -... - - - org.janusgraph - janusgraph-core - $MAVEN{project.version} - - - org.apache.tinkerpop - gremlin-driver - $MAVEN{tinkerpop.version} - -... - -... ----- - -. Add two configuration files, `conf/remote-graph.properties` and `conf/remote-objects.yaml`: -+ -.conf/remote-graph.properties -[source, changelog] ----- -gremlin.remote.remoteConnectionClass=org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection -gremlin.remote.driver.clusterFile=conf/remote-objects.yaml -gremlin.remote.driver.sourceName=g ----- -+ -.conf/remote-objects.yaml -[source, yaml] ----- -hosts: [localhost] -port: 8182 -serializer: { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, - config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }} ----- - -. Create a `GraphTraversalSource` which is the basis for all Gremlin traversals: -+ -[source, java] ----- -Graph graph = EmptyGraph.instance(); -GraphTraversalSource g = graph.traversal().withRemote("conf/remote-graph.properties"); -// Reuse 'g' across the application -// and close it on shut-down to close open connections with g.close() ----- - -. Execute a simple traversal: -+ -[source, java] ----- -Object herculesAge = g.V().has("name", "hercules").values("age").next(); -System.out.println("Hercules is " + herculesAge + " years old."); ----- -+ -`next()` is a terminal step that submits the traversal to the Gremlin Server and returns a single result. -Other terminal steps can be found http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#terminal-steps[in TinkerPop's reference documentation]. - -=== JanusGraph Specific Types and Predicates - -JanusGraph specific types and <> can be used directly from a Java application through the dependency `janusgraph-core`. diff --git a/docs/connecting-via-python.adoc b/docs/connecting-via-python.adoc deleted file mode 100644 index 9c06c08a92..0000000000 --- a/docs/connecting-via-python.adoc +++ /dev/null @@ -1,52 +0,0 @@ -[[connecting-via-python]] -== Connecting from Python - -JanusGraph can be queried from a Python application with Apache TinkerPop's http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#gremlin-python[Gremlin-Python]. - -Gremlin traversals can be constructed with Gremlin-Python just like in Gremlin-Java or Gremiln-Groovy. -Refer to <> for an introduction to Gremlin and pointers to further resources. - -IMPORTANT: Some Gremlin step and predicate names are reserved words in Python. -Those names are simply postfixed with `_` in Gremlin-Python, e.g., `in()` becomes `in_()`, `not()` becomes `not_()`, and so on. The other names affected by this are: `all`, `and`, `as`, `from`, `global`, `is`, `list`, `or`, and `set`. - -=== Getting Started with JanusGraph and Gremlin-Python - -To get started with Gremlin-Python: - -. Install Gremlin-Python: -[source, bash] -pip install gremlinpython==$MAVEN{tinkerpop.version} - -. Create a text file `gremlinexample.py` and add the following imports to it: -+ -[source, python] ----- -from gremlin_python import statics -from gremlin_python.structure.graph import Graph -from gremlin_python.process.graph_traversal import __ -from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection ----- - -. Create a `GraphTraversalSource` which is the basis for all Gremlin traversals: -+ -[source, python] ----- -graph = Graph() -connection = DriverRemoteConnection('ws://localhost:8182/gremlin', 'g') -// The connection should be closed on shut down to close open connections with connection.close() -g = graph.traversal().withRemote(connection) -// Reuse 'g' across the application ----- - -. Execute a simple traversal: -+ -[source, python] -herculesAge = g.V().has('name', 'hercules').values('age').next() -print('Hercules is {} years old.'.format(herculesAge)) -+ -`next()` is a terminal step that submits the traversal to the Gremlin Server and returns a single result. -Other terminal steps can be found http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#_remoteconnection_submission[in TinkerPop's reference documentation]. - -=== JanusGraph Specific Types and Predicates - -JanusGraph contains some types and <> that are not part of Apache TinkerPop and are therefore also not supported by Gremlin-Python. diff --git a/docs/connecting.adoc b/docs/connecting.adoc deleted file mode 100644 index e3b387ea1f..0000000000 --- a/docs/connecting.adoc +++ /dev/null @@ -1,16 +0,0 @@ -[[connecting]] -= Connecting to JanusGraph - -JanusGraph can be queried from all languages for which a TinkerPop driver exists. -Drivers allow sending of Gremlin traversals to a Gremlin Server like the <>. -A list of TinkerPop drivers is available on http://tinkerpop.apache.org/#language-drivers[TinkerPop's homepage]. - -In addition to drivers, there exist http://tinkerpop.apache.org/\#language-variants-compilers[query languages for TinkerPop] that make it easier to use Gremlin in different programming languages like Java, Python, or C#. -Some of these languages even construct Gremlin traversals from completely different query languages like Cypher or SPARQL. -Since JanusGraph implements TinkerPop, all of these languages can be used together with JanusGraph. - -include::connecting-via-java.adoc[] - -include::connecting-via-python.adoc[] - -include::connecting-via-dotnet.adoc[] diff --git a/docs/connecting/dotnet.md b/docs/connecting/dotnet.md new file mode 100644 index 0000000000..89368e6288 --- /dev/null +++ b/docs/connecting/dotnet.md @@ -0,0 +1,49 @@ +# Connecting from .NET + +Gremlin traversals can be constructed with Gremlin.Net just like in +Gremlin-Java or Gremiln-Groovy. Refer to [Gremlin Query Language](../basics/gremlin.md) for an +introduction to Gremlin and pointers to further resources. The main +syntactical difference for Gremlin.Net is that it follows .NET naming +conventions, e.g., method names use PascalCase instead of camelCase. + +## Getting Started with JanusGraph and Gremlin.Net + +To get started with Gremlin.Net: + +1. Create a console application: +```bash +dotnet new console -o GremlinExample +``` + +1. Add Gremlin.Net: +```bash +dotnet add package Gremlin.Net -v {{ tinkerpop_version }} +``` + +1. Create a `GraphTraversalSource` which is the basis for all Gremlin + traversals: +```csharp +var graph = new Graph(); +var client = new GremlinClient(new GremlinServer("localhost", 8182)); +// The client should be disposed on shut down to release resources +// and to close open connections with client.Dispose() +var g = graph.Traversal().WithRemote(new DriverRemoteConnection(client)); + // Reuse 'g' across the application +``` + +2. Execute a simple traversal: +```csharp +var herculesAge = g.V().Has("name", "hercules").Values("age").Next(); +Console.WriteLine($"Hercules is {herculesAge} years old."); +``` + The traversal can also be executed asynchronously by using + `Promise()` which is the recommended way as the underlying driver in + Gremlin.Net also works asynchronously: +```csharp +var herculesAge = await g.V().Has("name", "hercules").Values("age").Promise(t => t.Next()); +``` +## JanusGraph Specific Types and Predicates + +JanusGraph contains some types and [predicates](../index-backend/search-predicates.md) that +are not part of Apache TinkerPop and are therefore also not supported by +Gremlin.Net. diff --git a/docs/connecting/index.md b/docs/connecting/index.md new file mode 100644 index 0000000000..05bae893fb --- /dev/null +++ b/docs/connecting/index.md @@ -0,0 +1,12 @@ +JanusGraph can be queried from all languages for which a TinkerPop +driver exists. Drivers allow sending of Gremlin traversals to a Gremlin +Server like the [JanusGraph Server](../basics/server.md). A list of TinkerPop +drivers is available on [TinkerPop’s homepage](http://tinkerpop.apache.org/#language-drivers). + +In addition to drivers, there exist +[query languages for TinkerPop](http://tinkerpop.apache.org/#language-variants-compilers) +that make it easier to use Gremlin in different programming languages +like Java, Python, or C\#. Some of these languages even construct +Gremlin traversals from completely different query languages like Cypher +or SPARQL. Since JanusGraph implements TinkerPop, all of these languages +can be used together with JanusGraph. diff --git a/docs/connecting/java.md b/docs/connecting/java.md new file mode 100644 index 0000000000..b14466a447 --- /dev/null +++ b/docs/connecting/java.md @@ -0,0 +1,80 @@ +# Connecting from Java + + +While it is possible to embed JanusGraph as a library inside a Java +application and then directly connect to the backend, this section +assumes that the application connects to JanusGraph Server. For +information on how to embed JanusGraph, see the [JanusGraph Examples +projects](https://github.com/JanusGraph/janusgraph/tree/master/janusgraph-examples). + +This section only covers how applications can connect to JanusGraph +Server. Refer to [Gremlin Query Language](../basics/gremlin.md) for an introduction to Gremlin and +pointers to further resources. + +## Getting Started with JanusGraph and Gremlin-Java + +To get started with JanusGraph in Java: + +1. Create an application with Maven: +```bash +mvn archetype:generate -DgroupId=com.mycompany.project + -DartifactId=gremlin-example + -DarchetypeArtifactId=maven-archetype-quickstart + -DinteractiveMode=false +``` +2. Add dependencies on `janusgraph-core` and `gremlin-driver` to the dependency manager: + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + {{ latest_version }} + + + org.apache.tinkerpop + gremlin-driver + {{ tinkerpop_version }} + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:{{ latest_version }}" +compile "org.apache.tinkerpop:gremlin-driver:{{ tinkerpop_version }}" +``` + +3. Add two configuration files, `conf/remote-graph.properties` and + `conf/remote-objects.yaml`: + +```conf tab='conf/remote-graph.properties' +gremlin.remote.remoteConnectionClass=org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection +gremlin.remote.driver.clusterFile=conf/remote-objects.yaml +gremlin.remote.driver.sourceName=g +``` + +```yaml tab='conf/remote-objects.yaml' +hosts: [localhost] +port: 8182 +serializer: { + className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, + config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }} +``` + +4. Create a `GraphTraversalSource` which is the basis for all Gremlin traversals: +```java + Graph graph = EmptyGraph.instance(); + GraphTraversalSource g = graph.traversal().withRemote("conf/remote-graph.properties"); + // Reuse 'g' across the application + // and close it on shut-down to close open connections with g.close() +``` +5. Execute a simple traversal: +```java +Object herculesAge = g.V().has("name", "hercules").values("age").next(); +System.out.println("Hercules is " + herculesAge + " years old."); +``` +`next()` is a terminal step that submits the traversal to the Gremlin Server and returns a single result. + +## JanusGraph Specific Types and Predicates + +JanusGraph specific types and [predicates](../index-backend/search-predicates.md) can be +used directly from a Java application through the dependency +`janusgraph-core`. diff --git a/docs/connecting/python.md b/docs/connecting/python.md new file mode 100644 index 0000000000..8ae587e062 --- /dev/null +++ b/docs/connecting/python.md @@ -0,0 +1,53 @@ +# Connecting from Python + +Gremlin traversals can be constructed with Gremlin-Python just like in +Gremlin-Java or Gremiln-Groovy. Refer to [Gremlin Query Language](../basics/gremlin.md) for an +introduction to Gremlin and pointers to further resources. + +!!! important + Some Gremlin step and predicate names are reserved words in Python. + Those names are simply postfixed with `_` in Gremlin-Python, e.g., + `in()` becomes `in_()`, `not()` becomes `not_()`, and so on. The other + names affected by this are: `all`, `and`, `as`, `from`, `global`, + `is`, `list`, `or`, and `set`. + +## Getting Started with JanusGraph and Gremlin-Python + +To get started with Gremlin-Python: + +1. Install Gremlin-Python: +```bash +pip install gremlinpython=={{ tinkerpop_version }} +``` +1. Create a text file `gremlinexample.py` and add the following imports + to it: +```python +from gremlin_python import statics +from gremlin_python.structure.graph import Graph +from gremlin_python.process.graph_traversal import __ +from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection +``` + +2. Create a `GraphTraversalSource` which is the basis for all Gremlin + traversals: +```python +graph = Graph() +connection = DriverRemoteConnection('ws://localhost:8182/gremlin', 'g') +// The connection should be closed on shut down to close open connections with connection.close() +g = graph.traversal().withRemote(connection) +// Reuse 'g' across the application +``` + +3. Execute a simple traversal: +```python +herculesAge = g.V().has('name', 'hercules').values('age').next() +print('Hercules is {} years old.'.format(herculesAge)) +``` + `next()` is a terminal step that submits the traversal to the + Gremlin Server and returns a single result. + +## JanusGraph Specific Types and Predicates + +JanusGraph contains some types and [predicates](../index-backend/search-predicates.md) that +are not part of Apache TinkerPop and are therefore also not supported by +Gremlin-Python. diff --git a/docs/datamodel.adoc b/docs/datamodel.adoc deleted file mode 100644 index 130cde4e41..0000000000 --- a/docs/datamodel.adoc +++ /dev/null @@ -1,45 +0,0 @@ -[[data-model]] -== JanusGraph Data Model - -JanusGraph stores graphs in http://en.wikipedia.org/wiki/Adjacency_list[adjacency list format] which means that a graph is stored as a collection of vertices with their adjacency list. The adjacency list of a vertex contains all of the vertex's incident edges (and properties). - -By storing a graph in adjacency list format JanusGraph ensures that all of a vertex's incident edges and properties are stored compactly in the storage backend which speeds up traversals. The downside is that each edge has to be stored twice - once for each end vertex of the edge. - -In addition, JanusGraph maintains the adjacency list of each vertex in sort order with the order being defined by the sort key and sort order the edge labels. The sort order enables efficient retrievals of subsets of the adjacency list using <>. - -JanusGraph stores the adjacency list representation of a graph in any <> that supports the Bigtable data model. - -=== Bigtable Data Model - -[.tss-center] -image:bigtablemodel.png[] - -Under the http://en.wikipedia.org/wiki/Bigtable[Bigtable data model] each table is a collection of rows. Each row is uniquely identified by a key. Each row is comprised of an arbitrary (large, but limited) number of cells. A cell is composed of a column and value. A cell is uniquely identified by a column within a given row. Rows in the Bigtable model are called "wide rows" because they support a large number of cells and the columns of those cells don't have to be defined up front as is required in relational databases. - -JanusGraph has an additional requirement for the Bigtable data model: The cells must be sorted by their columns and a subset of the cells specified by a column range must be efficiently retrievable (e.g. by using index structures, skip lists, or binary search). - -In addition, a particular Bigtable implementation may keep the rows sorted in the order of their key. JanusGraph can exploit such key-order to effectively partition the graph which provides better loading and traversal performance for very large graphs. However, this is not a requirement. - -=== JanusGraph Data Layout - -[.tss-center] -image:storagelayout.png[] - -JanusGraph stores each adjacency list as a row in the underlying storage backend. The (64 bit) vertex id (which JanusGraph uniquely assigns to every vertex) is the key which points to the row containing the vertex's adjacency list. Each edge and property is stored as an individual cell in the row which allows for efficient insertions and deletions. -The maximum number of cells allowed per row in a particular storage backend is therefore also the maximum degree of a vertex that JanusGraph can support against this backend. - -If the storage backend supports key-order, the adjacency lists will be ordered by vertex id, and JanusGraph can assign vertex ids such that the graph is effectively partitioned. Ids are assigned such that vertices which are frequently co-accessed have ids with small absolute difference. - -=== Individual Edge Layout - -[.tss-center] -image:relationlayout.png[] - -Each edge and property is stored as one cell in the rows of its adjacent vertices. They are serialized such that the byte order of the column respects the sort key of the edge label. Variable id encoding schemes and compressed object serialization are used to keep the storage footprint of each edge/cell as small as possible. - -Consider the storage layout of an individual edge as visualized in the top row of the graphic above. The dark blue boxes represent numbers that are encoded with a variable length encoding scheme to reduce the number of bytes they consume. Red boxes represent one or multiple property values (i.e. objects) that are serialized with compressed meta data referenced in the associated property key. Grey boxes represent uncompressed property values (i.e. serialized objects). - -The serialized representation of an edge starts with the edge label's unique id (as assigned by JanusGraph). This is typically a small number and compressed well with variable id encoding. The last bit of this id is offset to store whether this is an incoming or outgoing edge. Next, the property value comprising the sort key are stored. The sort key is defined with the edge label and hence the sort key objects meta data can be referenced to the edge label. After that, the id of the adjacent vertex is stored. JanusGraph does not store the actual vertex id but the difference to the id of the vertex that owns this adjacency list. It is likely that the difference is a smaller number than the absolute id and hence compresses better. The vertex id is followed by the id of this edge. Each edge is assigned a unique id by JanusGraph. This concludes the column value of the edge's cell. The value of the edge's cell contains the compressed serialization of the signature properties of the edge (as defined by the label's signature key) and any other properties that have been added to the edge in uncompressed serialization. - -The serialized representation of a property is simpler and only contains the property's key id in the column. The property id and the property value are stored in the value. If the property key is defined as `list()`, however, the property id is stored in the column as well. - diff --git a/docs/deploymentscenarios.adoc b/docs/deploymentscenarios.adoc deleted file mode 100644 index 9683481db2..0000000000 --- a/docs/deploymentscenarios.adoc +++ /dev/null @@ -1,45 +0,0 @@ -[[deployment-scenarios]] -== Deployment Scenarios - -JanusGraph offers a wide choice of storage and index backends which results in great flexibility of how it can be deployed. This chapter presents a few possible deployment scenarios to help with the complexity that comes with this flexibility. - -Before discussing the different deployment scenarios, it is important to understand the roles of JanusGraph itself and that of the backends. First of all, applications only communicate directly with JanusGraph, mostly by sending Gremlin traversals for execution. JanusGraph then communicates with the configured backends to execute the received traversal. When JanusGraph is used in the form of JanusGraph Server, then there is nothing like a _master_ JanusGraph Server. Applications can therefore connect to any JanusGraph Server instance. They can also use a load-balancer to schedule requests to the different instances. The JanusGraph Server instances themselves don't communicate to each other directly which makes it easy to scale them when the need arises to process more traversals. - -NOTE: The scenarios presented in this chapter are only examples of how JanusGraph can be deployed. Each deployment needs to take into account the concrete use cases and production needs. - -[[getting-started-scenario]] -=== Getting Started Scenario - -This scenario is the scenario most users probably want to choose when they are just getting started with JanusGraph. It offers scalability and fault tolerance with a minimum number of servers required. JanusGraph Server runs together with an instance of the storage backend and optionally also an instance of the index backend on every server. - -image:getting-started-scenario.svg[Getting started deployment scenario diagram, 650] - -A setup like this can be extended by simply adding more servers of the same kind or by moving one of the components onto dedicated servers. The latter describes a growth path to transform the deployment into the <>. - -Any of the scalable storage backends can be used with this scenario. Note however that for Scylla http://docs.scylladb.com/getting-started/scylla_in_a_shared_environment/[some configuration is required when it is hosted co-located with other services] like in this scenario. When an index backend should be used in this scenario then it also needs to be one that is scalable. - -[[advanced-scenario]] -=== Advanced Scenario - -The advanced scenario is an evolution of the <>. Instead of hosting the JanusGraph Server instances together with the storage backend and optionally also the index backend, they are now separated on different servers. -The advantage of hosting the different components (JanusGraph Server, storage/index backend) on different servers is that they can be scaled and managed independently of each other. -This offers a higher flexibility at the cost of having to maintain more servers. - -image:advanced-scenario.svg[Advanced deployment scenario diagram, 800] - -Since this scenario offers independent scalability of the different components, it of course makes most sense to also use scalable backends. - -[[minimalist-scenario]] -=== Minimalist Scenario - -It is also possible to host JanusGraph Server together with the backend(s) on just one server. This is especially attractive for testing purposes or for example when JanusGraph just supports a single application which can then also run on the same server. - -image:minimalist-scenario.svg[Minimalist deployment scenario diagram, 650] - -Opposed to the previous scenarios, it makes most sense to use backends for this scenario that are not scalable. The in-memory backend can be used for testing purposes or Berkeley DB for production and Lucene as the optional index backend. - -[[embedded-janusgraph]] -=== Embedded JanusGraph - -Instead of connecting to the JanusGraph Server from an application it is also possible to embed JanusGraph as a library inside a JVM based application. While this reduces the administrative overhead, it makes it impossible to scale JanusGraph independently of the application. -Embedded JanusGraph can be deployed as a variation of any of the other scenarios. JanusGraph just moves from the server(s) directly into the application as its now just used as a library instead of an independent service. \ No newline at end of file diff --git a/docs/development.adoc b/docs/development.adoc deleted file mode 100644 index c3e40dc13a..0000000000 --- a/docs/development.adoc +++ /dev/null @@ -1,91 +0,0 @@ -[[development-process]] -= JanusGraph Development Process - -The following sections describe the JanusGraph development process. - - -[[development-decisions]] -== Development Decisions - -Many development decisions will be made during the day-to-day work of JanusGraph contributors without any extra process overhead. -However, for larger bodies of work and significant updates and additions, the following process shall be followed. -Significant work may include, but is not limited to: - -* A major new feature or subproject, e.g., a new storage adapter -* Incrementing the version of a core dependency such as a Apache TinkerPop -* Addition or deprecation of a public API -* Internal shared data structure or API change -* Addition of a new major dependency - -For these sorts of changes, contributors will: - -* Create one or more issues in the GitHub issue tracker -* Start a DISCUSS thread on the https://groups.google.com/forum/#!forum/janusgraph-dev[janusgraph-dev] list where the proposed change may be discussed by committers -and other community members -* When the proposer feels it appropriate, a VOTE shall be called -* Two +1 votes are required for the change to be accepted - -[[branching]] -== Branching - -For features that involve only one developer, developers will work in their own JanusGraph forks, managing their own branches, and submitting pull requests when ready. If multiple developers wish to collaborate on a feature, -they may request that a feature branch be created in JanusGraph repository. If the developers are not committers, a JanusGraph committer will be assigned as the shepherd for that branch. The shepherd will be responsible for merging pull requests to that branch. - -=== Branch Naming Conventions - -All branch names hosted in the JanusGraph repository shall be prepended with `Issue_#_`. - -[[pull-requests]] -== Pull Requests - -Users wishing to contribute to JanusGraph should fork the https://github.com/janusgraph/janusgraph[JanusGraph] repository and then submit pull requests using the https://help.github.com/articles/creating-a-pull-request/[GitHub pull request process]. -Every pull request must be associated with an existing issue. -Be sure to include the relevant issue number in the title of your pull request, complete the pull request template checklist, and provide a description of what test suites were run to validate the pull request. -Pull requests commit messages should clearly describe what work was accomplished. -Intermediate work-in-progress commits should be squashed prior to pull request submittal. - -[[review-then-commit]] -=== Review-Then-Commit (RTC) - -Pull requests must be reviewed before being merged following a review-then-commit (RTC) process. -The JanusGraph project uses the https://help.github.com/articles/about-pull-request-reviews/[GitHub review process] to review pull requests. -Non-committers are welcomed and encouraged to review pull requests. -Their review will be non-binding, but can be taken into consideration and are still very valuable community input. -The following rules apply to the voting process. - -* Approval flows -** 2 committer approvals are required to merge a pull request -** If a committer submits the pull request, it has their implicit approval so it requires 1 additional committer approval -** 1 committer approval followed a one week review period for objections at which point a lazy consensus is assumed -* One or more -1 votes within a change request will veto the pull request until the noted issue(s) are addressed and the -1 vote is withdrawn -* Change requests will not be considered valid without an explanation - -[[commit-then-review]] -=== Commit-Then-Review (CTR) - -In instances where the committer deems the full RTC process unnecessary, a commit-then-review (CTR) process may be employed. -The purpose of invoking CTR is to reduce the burden on the committers and minimize the turnaround time for merging trivial changes. -Changes of this sort may include: - -* Documentation typo or small documentation addition -* Addition of a new test case -* Merging approved fixes into an upstream branch - -Any commit that is made following CTR shall include the fact that it is a CTR in the commit comments. -Community members who wish to review CTRs may subscribe to the https://groups.google.com/forum/#!forum/janusgraph-commits[JanusGraph commits] list so that they will see CTRs as they come through. -If another committer responds with a -1, the commit should be rolled back and the formal RTC process should be followed. - - -[[release-policy]] -== Release Policy - -Any JanusGraph committer may propose a release. -To propose a release, simple start a new RELEASE thread on https://groups.google.com/forum/#!forum/janusgraph-dev[janusgraph-dev] proposing the new release and requesting feedback on what should be included in the release. -After consensus is reached the release manager will perform the following tasks: - -* Create a release branch so that work may continue on `master` -* Prepare the release artifacts -* Call a vote to approve the release on https://groups.google.com/forum/#!forum/janusgraph-dev[janusgraph-dev] -* Committers will be given 72 hours to review and vote on the release artifacts -* Three +1 votes are required for a release to be approved -* One or more -1 votes with explanation will veto the release until the noted issues are addressed and the -1 votes are withdrawn \ No newline at end of file diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000000..76f7da2391 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,215 @@ +The following sections describe the JanusGraph development process. + +## Development Decisions + +Many development decisions will be made during the day-to-day work of +JanusGraph contributors without any extra process overhead. However, for +larger bodies of work and significant updates and additions, the +following process shall be followed. Significant work may include, but +is not limited to: + +- A major new feature or subproject, e.g., a new storage adapter +- Incrementing the version of a core dependency such as a Apache TinkerPop +- Addition or deprecation of a public API +- Internal shared data structure or API change +- Addition of a new major dependency + +For these sorts of changes, contributors will: + +- Create one or more issues in the GitHub issue tracker +- Start a DISCUSS thread on the [janusgraph-dev](https://groups.google.com/forum/#!forum/janusgraph-dev) + list where the proposed change may be discussed by committers and other community members +- When the proposer feels it appropriate, a VOTE shall be called +- Two +1 votes are required for the change to be accepted + +## Branching + +For features that involve only one developer, developers will work in +their own JanusGraph forks, managing their own branches, and submitting +pull requests when ready. If multiple developers wish to collaborate on +a feature, they may request that a feature branch be created in +JanusGraph repository. If the developers are not committers, a +JanusGraph committer will be assigned as the shepherd for that branch. +The shepherd will be responsible for merging pull requests to that +branch. + +### Branch Naming Conventions + +All branch names hosted in the JanusGraph repository shall be prepended +with `Issue_#_`. + +## Pull Requests + +Users wishing to contribute to JanusGraph should fork the +[JanusGraph](https://github.com/janusgraph/janusgraph) repository and +then submit pull requests using the +[GitHub pull request process](https://help.github.com/articles/creating-a-pull-request/). +Every pull request must be associated with an existing issue. Be sure to +include the relevant issue number in the title of your pull request, +complete the pull request template checklist, and provide a description +of what test suites were run to validate the pull request. Pull requests +commit messages should clearly describe what work was accomplished. +Intermediate work-in-progress commits should be squashed prior to pull +request submittal. + +### Review-Then-Commit (RTC) + +Pull requests must be reviewed before being merged following a +review-then-commit (RTC) process. The JanusGraph project uses the +[GitHub review process](https://help.github.com/articles/about-pull-request-reviews/) +to review pull requests. Non-committers are welcomed and encouraged to +review pull requests. Their review will be non-binding, but can be taken +into consideration and are still very valuable community input. The +following rules apply to the voting process. + +- Approval flows + - 2 committer approvals are required to merge a pull request + - If a committer submits the pull request, it has their implicit + approval so it requires 1 additional committer approval + - 1 committer approval followed a one week review period for + objections at which point a lazy consensus is assumed +- One or more -1 votes within a change request will veto the pull + request until the noted issue(s) are addressed and the -1 vote is + withdrawn +- Change requests will not be considered valid without an explanation + +### Commit-Then-Review (CTR) + +In instances where the committer deems the full RTC process unnecessary, +a commit-then-review (CTR) process may be employed. The purpose of +invoking CTR is to reduce the burden on the committers and minimize the +turnaround time for merging trivial changes. Changes of this sort may +include: + +- Documentation typo or small documentation addition +- Addition of a new test case +- Merging approved fixes into an upstream branch + +Any commit that is made following CTR shall include the fact that it is +a CTR in the commit comments. Community members who wish to review CTRs +may subscribe to the [JanusGraph commits](https://groups.google.com/forum/#!forum/janusgraph-commits) +list so that they will see CTRs as they come through. If another +committer responds with a -1, the commit should be rolled back and the +formal RTC process should be followed. + +### Merging of Pull Requests + +A pull request is ready to be merged when it has been approved (see +[Review-Then-Commit (RTC)](#review-then-commit)). It can either be +merged manually with `git` commands or through the GitHub UI with the +*Merge pull request* button. If the pull request targets a release +branch that is upstream of other release branches (e.g., `0.2` is +upstream of `master`), then it is important to make sure that the change +also lands in the downstream release branches. This is the +responsibility of the committer who merges the pull request. Therefore, +merge the pull request first into its target branch and then merge that +branch (e.g., `0.2`) into the next downstream branch (e.g., `0.3`), and +so on until you merge the last release branch into `master`. Afterwards +push all release branches, ideally with an atomic commit: + +```bash +git push --atomic origin master 0.3 0.2 +``` + +This approach keeps merge conflicts to a minimum when a release branch +is merged into a downstream release branch. + +## Release Policy + +Any JanusGraph committer may propose a release. To propose a release, +simple start a new RELEASE thread on +[janusgraph-dev](https://groups.google.com/forum/#!forum/janusgraph-dev) +proposing the new release and requesting feedback on what should be +included in the release. After consensus is reached the release manager +will perform the following tasks: + +- Create a release branch so that work may continue on `master` +- Prepare the release artifacts +- Call a vote to approve the release on + [janusgraph-dev](https://groups.google.com/forum/#!forum/janusgraph-dev) +- Committers will be given 72 hours to review and vote on the release + artifacts +- Three +1 votes are required for a release to be approved +- One or more -1 votes with explanation will veto the release until + the noted issues are addressed and the -1 votes are withdrawn + +## Building JanusGraph + +To build JanusGraph you need [git](http://git-scm.com/) and +[Maven](http://maven.apache.org/). + +1. Clone the [JanusGraph repository from + GitHub](https://github.com/JanusGraph/janusgraph) to a local + directory. + +2. In that directory, execute `mvn clean install`. This will build + JanusGraph and run the internal test suite. The internal test suite + has no external dependencies. Note, that running all test cases + requires a significant amount of time. To skip the tests when + building JanusGraph, execute `mvn clean install -DskipTests` + +3. For comprehensive test coverage, execute + `mvn clean test -P comprehensive`. This will run additional test + covering communication to external storage backends, performance + tests and concurrency tests. The comprehensive test suite uses + Cassandra and HBase as external databases and requires that + Cassandra and HBase are installed. Note, that running the + comprehensive test suite requires a significant amount of of time + (> 1 hour). + +### Depending on JanusGraph Snapshots + +For developing against the most current version of JanusGraph, depend on +JanusGraph snapshot releases. Note, that these releases are development +releases and therefore unstable and likely to change. Unless one is +interested in the most recent development status of JanusGraph, we +recommend to use the stable JanusGraph release instead. + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + {{ snapshot_version }} + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:{{ snapshot_version }}" +``` + +Check the [master branch](https://github.com/JanusGraph/janusgraph/tree/master) for the +most current release version. SNAPSHOTs will be available through the +[Sonatype repository](https://oss.sonatype.org/content/repositories/snapshots/org/janusgraph/). + +When adding this dependency, be sure to add the following repository to build file: + +```xml tab='Maven' + + ossrh + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + +``` + +```groovy tab='Gradle' +maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + mavenContent { + snapshotsOnly() + } +} +``` + +### FAQs + +**Maven build causes dozens of "\[WARNING\] We have a duplicate…" +errors** + +Make sure to use the maven-assembly-plugin when building or depending on +JanusGraph. \ No newline at end of file diff --git a/docs/directindex.adoc b/docs/directindex.adoc deleted file mode 100644 index 850f2dcc7e..0000000000 --- a/docs/directindex.adoc +++ /dev/null @@ -1,80 +0,0 @@ -[[direct-index-query]] -== Direct Index Query - -JanusGraph's standard global graph querying mechanism supports boolean queries for vertices or edges. In other words, an element either matches the query or it does not. There are no partial matches or result scoring. - -Some indexing backends additionally support fuzzy search queries. For those queries, a score is computed for each match to indicate the "goodness" of the match and results are returned in the order of their score. Fuzzy search is particularly useful when dealing with full-text search queries where matching more words is considered to be better. - -Since fuzzy search implementations and scoring algorithms differ significantly between indexing backends, JanusGraph does not support fuzzy search natively. However, JanusGraph provides a _direct index query_ mechanism that allows search queries to be directly send to the indexing backend for evaluation (for those backends that support it). - -Use `Graph.indexQuery()` to compose a query that is executed directly against an indexing backend. This query builder expects two parameters: - -. The name of the indexing backend to query. This must be the name configured in JanusGraph's configuration and used in the property key indexing definitions -. The query string - -The builder allows configuration of the maximum number of elements to be returned via its `limit(int)` method. The builder's `offset(int)` controls number of initial matches in the result set to skip. To retrieve all vertex or edges matching the given query in the specified indexing backend, invoke `vertices()` or `edges()`, respectively. It is not possible to query for both vertices and edges at the same time. -These methods return an `Iterable` over `Result` objects. A result object contains the matched handle, retrievable via `getElement()`, and the associated score - `getScore()`. - -Consider the following example: - -[source, java] -ManagementSystem mgmt = graph.openManagement(); -PropertyKey text = mgmt.makePropertyKey("text").dataType(String.class).make(); -mgmt.buildIndex("vertexByText", Vertex.class).addKey(text).buildMixedIndex("search"); -mgmt.commit(); -// ... Load vertices ... -for (Result result : graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertices()) { - System.out.println(result.getElement() + ": " + result.getScore()); -} - -=== Query String - -The query string is handed directly to the indexing backend for processing and hence the query string syntax depends on what is supported by the indexing backend. For vertex queries, JanusGraph will analyze the query string for property key references starting with "v." and replace those by a handle to the indexing field that corresponds to the property key. Likewise, for edge queries, JanusGraph will replace property key references starting with "e.". -Hence, to refer to a property of a vertex, use "v.[KEY_NAME]" in the query string. Likewise, for edges write "e.[KEY_NAME]". - -<> and <> support the http://lucene.apache.org/core/4_10_4/queryparser/org/apache/lucene/queryparser/classic/package-summary.html[Lucene query syntax]. Refer to the http://lucene.apache.org/core/4_1_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html[Lucene documentation] or the http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html[Elasticsearch documentation] for more information. The query used in the example above follows the Lucene query syntax. - -[source, java] -graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertices() - -This query matches all vertices where the text contains any of the three words (grouped by parentheses) and score matches higher the more words are matched in the text. - -In addition <> supports wildcard queries, use "v.\*" or "e.*" in the query string to query if any of the properties on the element match. - -=== Query Totals - -It is sometimes useful to know how many total results were returned from a query without having to retrieve all results. Fortunately, Elasticsearch and Solr provide a shortcut that does not involve retrieving and ranking all documents. This shortcut is exposed through the ".vertexTotals()", ".edgeTotals()", and ".propertyTotals()" methods. - -The totals can be retrieved using the same query syntax as the indexQuery builder, but size is overwritten to be 0. - -[source, java] -graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertexTotals() - -=== Gotchas - -==== Property Key Names - -Names of property keys that contain non-alphanumeric characters must be placed in quotation marks to ensure that the query is parsed correctly. - -[source, java] -graph.indexQuery("vertexByText", "v.\"first_name\":john").vertices() - -Some property key names may be transformed by the JanusGraph indexing backend implementation. For instance, an indexing backend that does not permit spaces in field names may transform "My Field Name" to "My•Field•Name", or an indexing backend like Solr may append type information to the name, transforming "myBooleanField" to "myBooleanField_b". These transformations happen in the index backend's implementation of IndexProvider, in the "mapKey2Field" method. Indexing backends may reserve special characters (such as '•') and prohibit indexing of fields that contain them. For this reason it is recommended to avoid spaces and special characters in property names. - -In general, making direct index queries depends on implementation details of JanusGraph indexing backends that are normally hidden from users, so it's best to verify a query empirically against the indexing backend in use. - -==== Element Identifier Collision - -The strings "v.", "e.", and "p." are used to identify a vertex, edge or property element respectively in a query. If the field name or the query value contains the same sequence of characters, this can cause a collision in the query string and parsing errors as in the following example: - -[source, java] -graph.indexQuery("vertexByText", "v.name:v.john").vertices() //DOES NOT WORK! - -To avoid such identifier collisions, use the `setElementIdentifier` method to define a unique element identifier string that does not occur in any other parts of the query: - -[source, java] -graph.indexQuery("vertexByText", "$v$name:v.john").setElementIdentifier("$v$").vertices() - -==== Mixed Index Availability Delay - -When a query traverses a <> immediately after data is inserted the changes may not be visible. In <> the configuration option that determines this delay is https://www.elastic.co/guide/en/elasticsearch/reference/5.4/index-modules.html#dynamic-index-settings[index refresh interval]. In <> the primary configuration option is https://lucene.apache.org/solr/guide/6_6/near-real-time-searching.html[max time]. diff --git a/docs/doc-versions.adoc b/docs/doc-versions.adoc deleted file mode 100644 index fec755541e..0000000000 --- a/docs/doc-versions.adoc +++ /dev/null @@ -1,14 +0,0 @@ -[[doc-versions]] -[appendix] -== Other documentation versions -* https://docs.janusgraph.org/latest/index.html[Latest] -* https://docs.janusgraph.org/0.3.1/index.html[Version 0.3.1] -* https://docs.janusgraph.org/0.3.0/index.html[Version 0.3.0] -* https://docs.janusgraph.org/0.2.3/index.html[Version 0.2.3] -* https://docs.janusgraph.org/0.2.2/index.html[Version 0.2.2] -* https://docs.janusgraph.org/0.2.1/index.html[Version 0.2.1] -* https://docs.janusgraph.org/0.2.0/index.html[Version 0.2.0] -* https://docs.janusgraph.org/0.1.1/index.html[Version 0.1.1] -* https://docs.janusgraph.org/0.1.0/index.html[Version 0.1.0] - -Please visit the latest online version of this page to https://docs.janusgraph.org/latest/doc-versions.html[see a complete list of docs versions]. diff --git a/docs/elasticsearch.adoc b/docs/elasticsearch.adoc deleted file mode 100644 index 0a86fa2896..0000000000 --- a/docs/elasticsearch.adoc +++ /dev/null @@ -1,159 +0,0 @@ -[[elasticsearch]] -== Elasticsearch - -[quote, 'https://www.elastic.co/products/elasticsearch/[Elasticsearch Overview]'] -Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases. As the heart of the Elastic Stack, it centrally stores your data so you can discover the expected and uncover the unexpected. - -JanusGraph supports https://www.elastic.co/[Elasticsearch] as an index backend. Here are some of the Elasticsearch features supported by JanusGraph: - -* *Full-Text*: Supports all `Text` predicates to search for text properties that matches a given word, prefix or regular expression. -* *Geo*: Supports all `Geo` predicates to search for geo properties that are intersecting, within, disjoint to or contained in a given query geometry. Supports points, circles, boxes, lines and polygons for indexing. Supports circles, boxes and polygons for querying point properties and all shapes for querying non-point properties. -* *Numeric Range*: Supports all numeric comparisons in `Compare`. -* *Flexible Configuration*: Supports remote operation and open-ended settings customization. -* *Collections*: Supports indexing SET and LIST cardinality properties. -* *Temporal*: Nanosecond granularity temporal indexing. -* *Custom Analyzer*: Choose to use a custom analyzer - -Please see <> for details on what versions of ES will work with JanusGraph. - -[IMPORTANT] -=============================== -Beginning with Elasticsearch 5.0 JanusGraph uses sandboxed https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-painless.html[Painless scripts] for inline updates, which are enabled by default in Elasticsearch 5.x. - -Using JanusGraph with Elasticsearch 2.x requires enabling Groovy inline scripting by setting `script.engine.groovy.inline.update` to `true` on the Elasticsearch cluster (see https://www.elastic.co/guide/en/elasticsearch/reference/2.3/modules-scripting.html#enable-dynamic-scripting[dynamic scripting documentation] for more information). -=============================== - -=== Running Elasticsearch - -JanusGraph supports connections to a running Elasticsearch cluster. JanusGraph provides two options for running local Elasticsearch instances for getting started quickly. JanusGraph server (see <>) automatically starts a local Elasticsearch instance. Alternatively JanusGraph releases include a full Elasticsearch distribution to allow users to manually start a local Elasticsearch instance (see https://www.elastic.co/guide/en/elasticsearch/guide/current/running-elasticsearch.html[this page] for more information). - -[source,bourne] ----- -$ elasticsearch/bin/elasticsearch ----- - -[NOTE] -For security reasons Elasticsearch must be run under a non-root account - -=== Elasticsearch Configuration Overview - -JanusGraph supports HTTP client connections to a running Elasticsearch cluster. Please see <> for details on what versions of ES will work with the different client types in JanusGraph. - -[NOTE] -JanusGraph's index options start with the string "`index.[X].`" where "`[X]`" is a user-defined name for the backend. This user-defined name must be passed to JanusGraph's ManagementSystem interface when building a mixed index, as described in <>, so that JanusGraph knows which of potentially multiple configured index backends to use. Configuration snippets in this chapter use the name `search`, whereas prose discussion of options typically write `[X]` in the same position. The exact index name is not significant as long as it is used consistently in JanusGraph's configuration and when administering indices. - -[TIP] -It's recommended that index names contain only alphanumeric lowercase characters and hyphens, and that they start with a lowercase letter. - -==== Connecting to Elasticsearch - -The Elasticsearch client is specified as follows: - -[source, properties] ----- -index.search.backend=elasticsearch ----- - - -When connecting to Elasticsearch a single or list of hostnames for the Elasticsearch instances must be provided. These are supplied via JanusGraph's `index.[X].hostname` key. - -[source, properties] ----- -index.search.backend=elasticsearch -index.search.hostname=10.0.0.10:9200 ----- - -Each host or host:port pair specified here will be added to the HTTP client's round-robin list of request targets. Here's a minimal configuration that will round-robin over 10.0.0.10 on the default Elasticsearch HTTP port (9200) and 10.0.0.20 on port 7777: - -[source, properties] ----- -index.search.backend=elasticsearch -index.search.hostname=10.0.0.10, 10.0.0.20:7777 ----- - -===== JanusGraph `index.[X]` and `index.[X].elasticsearch` options - -JanusGraph only uses default values for `index-name` and `health-request-timeout`. See <> for descriptions of these options and their accepted values. - -* `index.[X].elasticsearch.index-name` -* `index.[X].elasticsearch.health-request-timeout` - -[[es-cfg-rest-opts]] -==== REST Client Options - -The REST client accepts the `index.[X].bulk-refresh` option. This option controls when changes are made visible to search. See https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html[?refresh documentation] for more information. - -==== Ingest Pipelines -If using Elasticsearch 5.0 or higher, a different ingest pipelines can be set for each mixed index. -Ingest pipeline can be use to pre-process documents before indexing. A pipeline is composed by a series of processors. Each processor transforms the document in some way. -For example https://www.elastic.co/guide/en/elasticsearch/reference/current/date-processor.html[date processor] can extract a date from a text to a date field. So you can query this date with JanusGraph without it being physically in the primary storage. - -* `index.[X].elasticsearch.ingest-pipeline.[mixedIndexName] = pipeline_id` - -See https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html[ingest documentation] for more information about ingest pipelines and https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-processors.html[processors documentation] for more information about ingest processors. - -=== Secure Elasticsearch - -Elasticsearch does not perform authentication or authorization. A client that can connect to ES is trusted by ES. When Elasticsearch runs on an unsecured or public network, particularly the Internet, it should be deployed with some type of external security. This is generally done with a combination of firewalling and tunneling of Elasticsearch's ports. Elasticsearch has two client-facing ports to consider: - -* The HTTP REST API, usually on port 9200 -* The native "transport" protocol, usually on port 9300 - -A client uses either one protocol/port or the other, but not both simultaneously. Securing the HTTP protocol port is generally done with a combination of firewalling and a reverse proxy with SSL encryption and HTTP authentication. There are a couple of ways to approach security on the native "transport" protocol port: - -Tunnel ES's native "transport" protocol:: This approach can be implemented with SSL/TLS tunneling (for instance via https://www.stunnel.org/index.html[stunnel]), a VPN, or SSH port forwarding. SSL/TLS tunnels require non-trivial setup and monitoring: one or both ends of the tunnel need a certificate, and the stunnel processes need to be configured and running continuously. The setup for most secure VPNs is likewise non-trivial. Some Elasticsearch service providers handle server-side tunnel management and provide a custom Elasticsearch `transport.type` to simplify the client setup. -Add a firewall rule that allows only trusted clients to connect on Elasticsearch's native protocol port:: This is typically done at the host firewall level. Easy to configure, but very weak security by itself. - -[[es-cfg-index-create]] -=== Index Creation Options - -JanusGraph supports customization of the index settings it uses when creating its Elasticsearch index. It allows setting arbitrary key-value pairs on the `settings` object in the https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html[Elasticsearch `create index` request] issued by JanusGraph. Here is a non-exhaustive sample of Elasticsearch index settings that can be customized using this mechanism: - -* `index.number_of_replicas` -* `index.number_of_shards` -* `index.refresh_interval` - -Settings customized through this mechanism are only applied when JanusGraph attempts to create its index in Elasticsearch. If JanusGraph finds that its index already exists, then it does not attempt to recreate it, and these settings have no effect. - -==== Embedding ES index creation settings with `create.ext` - -JanusGraph iterates over all properties prefixed with `index.[X].elasticsearch.create.ext.`, where `[X]` is an index name such as `search`. It strips the prefix from each property key. The remainder of the stripped key will be interpreted as an Elasticsearch index creation setting. The value associated with the key is not modified. The stripped key and unmodified value are passed as part of the `settings` object in the Elasticsearch create index request that JanusGraph issues when bootstrapping on ES. This allows embedding arbitrary index creation settings settings in JanusGraph's properties. Here's an example configuration fragment that customizes three Elasticsearch index settings using the `create.ext` config mechanism: - -[source, properties] ----- -index.search.backend=elasticsearch -index.search.elasticsearch.create.ext.number_of_shards=15 -index.search.elasticsearch.create.ext.number_of_replicas=3 -index.search.elasticsearch.create.ext.shard.check_on_startup=true ----- - -The configuration fragment listed above takes advantage of Elasticsearch's assumption, implemented server-side, that unqualified `create index` setting keys have an `index.` prefix. It's also possible to spell out the index prefix explicitly. Here's a JanusGraph config file functionally equivalent to the one listed above, except that the `index.` prefix before the index creation settings is explicit: - -[source, properties] ----- -index.search.backend=elasticsearch -index.search.elasticsearch.create.ext.index.number_of_shards=15 -index.search.elasticsearch.create.ext.index.number_of_replicas=3 -index.search.elasticsearch.create.ext.index.shard.check_on_startup=false ----- - -[TIP] -The `create.ext` mechanism for specifying index creation settings is compatible with JanusGraph's Elasticsearch configuration. - -=== Troubleshooting - -==== Connection Issues to remote Elasticsearch cluster - -Check that the Elasticsearch cluster nodes are reachable on the HTTP protocol port from the JanusGraph nodes. Check the node listen port by examining the Elasticsearch node configuration logs or using a general diagnostic utility like `netstat`. Check the JanusGraph configuration. - -=== Optimizing Elasticsearch - -==== Write Optimization - -For <> or other write-intense applications, consider increasing Elasticsearch's refresh interval. Refer to https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html[this discussion] on how to increase the refresh interval and its impact on write performance. Note, that a higher refresh interval means that it takes a longer time for graph mutations to be available in the index. - -For additional suggestions on how to increase write performance in Elasticsearch with detailed instructions, please read http://blog.bugsense.com/post/35580279634/indexing-bigdata-with-elasticsearch[this blog post]. - -==== Further Reading - -* Please refer to the https://www.elastic.co[Elasticsearch homepage] and available documentation for more information on Elasticsearch and how to setup an Elasticsearch cluster. diff --git a/docs/eventualconsistency.adoc b/docs/eventualconsistency.adoc deleted file mode 100644 index aee138c78b..0000000000 --- a/docs/eventualconsistency.adoc +++ /dev/null @@ -1,109 +0,0 @@ -[[eventual-consistency]] -== Eventually-Consistent Storage Backends - -When running JanusGraph against an eventually consistent storage backend special JanusGraph features must be used to ensure data consistency and special considerations must be made regarding data degradation. - -This page summarizes some of the aspects to consider when running JanusGraph on top of an eventually consistent storage backend like Apache Cassandra or Apache HBase. - -=== Data Consistency - -On eventually consistent storage backends, JanusGraph must obtain locks in order to ensure consistency because the underlying storage backend does not provide transactional isolation. In the interest of efficiency, JanusGraph does not use locking by default. Hence, the user has to decide for each schema element that defines a consistency constraint whether or not to use locking. Use `JanusGraphManagement.setConsistency(element, ConsistencyModifier.LOCK)` to explicitly enable locking on a schema element as shown in the following examples. - -[source, gremlin] -mgmt = graph.openManagement() -name = mgmt.makePropertyKey('consistentName').dataType(String.class).make() -index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex() -mgmt.setConsistency(name, ConsistencyModifier.LOCK) // Ensures only one name per vertex -mgmt.setConsistency(index, ConsistencyModifier.LOCK) // Ensures name uniqueness in the graph -mgmt.commit() - -When updating an element that is guarded by a uniqueness constraint, JanusGraph uses the following protocol at the end of a transaction when calling `tx.commit()`: - -. Acquire a lock on all elements that have a consistency constraint -. Re-read those elements from the storage backend and verify that they match the state of the element in the current transaction prior to modification. If not, the element was concurrently modified and a PermanentLocking exception is thrown. -. Persist the state of the transaction against the storage backend. -. Release all locks. - -This is a brief description of the locking protocol which leaves out optimizations (e.g. local conflict detection) and detection of failure scenarios (e.g. expired locks). - -The actual lock application mechanism is abstracted such that JanusGraph can use multiple implementations of a locking provider. Currently, two locking providers are supported in the JanusGraph distribution: - -. A locking implementation based on key-consistent read and write operations that is agnostic to the underlying storage backend as long as it supports key-consistent operations (which includes Cassandra and HBase). This is the default implementation and uses timestamp based lock applications to determine which transaction holds the lock. -. A Cassandra specific locking implementation based on the Astyanax locking recipe. - -Both locking providers require that clocks are synchronized across all machines in the cluster. - -[WARNING] -The locking implementation is not robust against all failure -scenarios. For instance, when a Cassandra cluster drops below quorum, -consistency is no longer ensured. Hence, it is suggested to use -locking-based consistency constraints sparingly with eventually -consistent storage backends. For use cases that require strict and or -frequent consistency constraint enforcement, it is suggested to use a -storage backend that provides transactional isolation. - -==== Data Consistency without Locks - -Because of the additional steps required to acquire a lock when committing a modifying transaction, locking is a fairly expensive way to ensure consistency and can lead to deadlock when very many concurrent transactions try to modify the same elements in the graph. Hence, locking should be used in situations where consistency is more important than write latency and the number of conflicting transactions is small. - -In other situations, it may be better to allow conflicting transactions to proceed and to resolve inconsistencies at read time. This is a design pattern commonly employed in large scale data systems and most effective when the actual likelihood of conflict is small. Hence, write transactions don't incur additional overhead and any (unlikely) conflict that does occur is detected and resolved at read time and later cleaned up. JanusGraph makes it easy to use this strategy through the following features. - -===== Forking Edges - -Because edge are stored as single records in the underlying storage backend, concurrently modifying a single edge would lead to conflict. Instead of locking, an edge label can be configured to use `ConsistencyModifier.FORK`. The following example creates a new edge label `related` and defines its consistency to FORK. - -[source, gremlin] -mgmt = graph.openManagement() -related = mgmt.makeEdgeLabel('related').make() -mgmt.setConsistency(related, ConsistencyModifier.FORK) -mgmt.commit() - -When modifying an edge whose label is configured to FORK the edge is deleted and the modified edge is added as a new one. Hence, if two concurrent transactions modify the same edge, two modified copies of the edge will exist upon commit which can be resolved during querying traversals if needed. - -[NOTE] -Edge forking only applies to MULTI edges. Edge labels with a multiplicity constraint cannot use this strategy since a constraint is built into the edge label definition that requires an explicit lock or use the conflict resolution mechanism of the underlying storage backend. - -===== Multi-Properties - -Modifying single valued properties on vertices concurrently can result in a conflict. Similarly to edges, one can allow an arbitrary number of properties on a vertex for a particular property key defined with cardinality LIST and FORK on modification. Hence, instead of conflict one reads multiple properties. Since JanusGraph allows properties on properties, provenance information like `author` can be added to the properties to facilitate resolution at read time. - -See <> to learn how to define those. - -=== Data Inconsistency - -==== Temporary Inconsistency - -On eventually consistent storage backends, writes may not be immediately visible to the entire cluster causing temporary inconsistencies in the graph. This is an inherent property of eventual consistency, in the sense, that accepted updates must be propagated to other instances in the cluster and no guarantees are made with respect to read atomicity in the interest of performance. - -From JanusGraph's perspective, eventual consistency might cause the following temporary graph inconsistencies in addition the general inconsistency that some parts of a transaction are visible while others aren't yet. - -*Stale Index entries*:: Index entries might point to nonexistent -vertices or edges. Similarly, a vertex or edge appears in the graph but is not yet indexed and hence ignored by global graph queries. - -*Half-Edges*:: Only one direction of an edge gets persisted or deleted -which might lead to the edge not being or incorrectly being retrieved. - -[NOTE] -In order to avoid that write failures result in permanent inconsistencies in the graph it is recommended to use storage backends that support batch write atomicity and to ensure that write atomicity is enabled. To get the benefit of write atomicity, the number modifications made in a single transaction must be smaller than the configured `buffer-size` option documented in <>. The buffer size defines the maximum number of modifications that JanusGraph will persist in a single batch. If a transaction has more modifications, the persistence will be split into multiple batches which are persisted individually which is useful for batch loading but invalidates write atomicity. - -[[ghost-vertices]] -==== Ghost Vertices - -A permanent inconsistency that can arise when operating JanusGraph on eventually consistent storage backend is the phenomena of *ghost vertices*. If a vertex gets deleted while it is concurrently -being modified, the vertex might re-appear as a _ghost_. - -The following strategies can be used to mitigate this issue: - -*Existence checks*:: Configure transactions to (double) check for the -existence of vertices prior to returning them. Please see -<> for more information and note that this can -significantly decrease performance. Note, that this does not fix the -inconsistencies but hides some of them from the user. - -*Regular Clean-ups*:: Run regular batch-jobs to repair inconsistencies -in the graph using <>. This is the only strategy that -can address all inconsistencies and effectively repair them. We will -provide increasing support for such repairs in future versions of -Faunus. - -*Soft Deletes*:: Instead of deleting vertices, they are marked as deleted which keeps them in the graph for future analysis but hides them from user-facing transactions. diff --git a/docs/exampleconfig.adoc b/docs/exampleconfig.adoc deleted file mode 100644 index 773483a7e8..0000000000 --- a/docs/exampleconfig.adoc +++ /dev/null @@ -1,75 +0,0 @@ -[[example-config]] -=== Example Graph Configuration - -This page illustrates a number of common graph configurations. Please refer to <> and the pages of the respective <> <> for more information. - -Also, note that the JanusGraph distribution includes local configuration files in the `conf/` directory. - -==== BerkeleyDB - -[source, properties] ----- -storage.backend=berkeleyje -storage.directory=/tmp/graph - -index.search.backend=elasticsearch -index.search.directory=/tmp/searchindex -index.search.elasticsearch.client-only=false -index.search.elasticsearch.local-mode=true ----- - -This configuration file configures JanusGraph to use <> as an embedded storage backend, meaning, JanusGraph will start BerkeleyDB internally. The primary data will be stored in the directory `/tmp/graph`. - -In addition, this configures an embedded <> index backend with the name `search`. JanusGraph will start Elasticsearch internally and it will not be externally accessible since `local-mode` is enabled. Elasticsearch stores all data for the `search` index in `/tmp/searchindex`. Configuring an index backend is optional. - -==== Cassandra - -===== Cassandra Remote - -[source, properties] ----- -storage.backend=cql -storage.hostname=100.100.100.1, 100.100.100.2 - -index.search.backend=elasticsearch -index.search.hostname=100.100.101.1, 100.100.101.2 -index.search.elasticsearch.client-only=true ----- - -This configuration file configures JanusGraph to use <> as a remote storage backend. It assumes that a Cassandra cluster is running and accessible at the given IP addresses. If Cassandra is running locally, use the IP address `127.0.0.1`. - -In addition, this configures a remote <> index backend with the name `search`. It assumes that an Elasticsearch cluster is running and accessible at the given IP addresses. Enabling `client-only` ensures that the local instance does not join the existing Elasticsearch cluster as another node but only connects to it. Configuring an index backend is optional. - -===== Embedded Cassandra - -[source, properties] ----- -storage.backend=embeddedcassandra -storage.conf-file=config/cassandra.yaml - -index.search.backend=elasticsearch -index.search.directory=/tmp/searchindex -index.search.elasticsearch.client-only=false -index.search.elasticsearch.local-mode=true ----- - -This configuration file configures JanusGraph to start <> internally embedded in JanusGraph and specifies the yaml configuration file for Cassandra. Cassandra is still accessible externally and can connect to other available Cassandra nodes to form a cluster as configured in the yaml file. - -The optional index backend configuration is identical to embedded index configuration described above. - -==== HBase - -[source, properties] ----- -storage.backend=hbase -storage.hostname=127.0.0.1 -storage.port=2181 - -index.search.backend=elasticsearch -index.search.hostname=127.0.0.1 -index.search.elasticsearch.client-only=true ----- - -This configuration file configures JanusGraph to use <> as a remote storage backend. It assumes that an HBase cluster is running and accessible at the given IP addresses through the configured port. If HBase is running locally, use the IP address `127.0.0.1`. - -The optional index backend configuration is identical to remote index configuration described above. diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..830326e7fd367d2d8bcbd9e38b906984d353090d GIT binary patch literal 1371 zcmV-h1*H0kP)wj#kx2D)cI$mP13cI78JW$-Ig?I z+QzjwAF7}t6@68NJxoxcW#Ws<7z*OcgduhM;M``{Ch_|__jl**C1pv;fltr5_n!0n zcYeQfQmUbuzz>XDd`Dm3W>!!d0o%ZKa2p7E*VMxh!y{xj7z6WQ0W5RIjx|1UF64>HIbG7|H=Lwm=4yEh^ z^CEZL8wK~n0h#nWXscwYl}nN}7AGs8K%X#xX^ka$tcm86B+VIOuHAmCvQx3QuD}R7?Lp6Xg8_R9>&14VP)fZ-$T)fmo)wz?PRWx*jc^*A%hHq zOZs&{;hs)k;L)9AjrXpw^k*?(pEqNh-s~j{-l?&hp7Zm1a5sbm8a_VGM)`ryi^})< zn~v<$k=*>#CmYpNsgeC?_=_-(1 z$muHB1a^X^=;TqUf_No@VsuvWh=81dm()*Mog^bE(jL%Rxo<2HVnT=kwRYp&)jNt3z)Jjl*sxMj{u@Hx2DEX=C#>yS z839i&EPw~RzXw#IkDNUOeqdTiLIn_7{T>P#9{1bInZz^BIwf1ST5AN~h!K}Tr=1Ok z!G9yKKFbwhaJg9%V5(myCGaaGWaOOz3VrYzze}@n@jl~(zBSdQl{coltNu;64BiLZ z0o_$Da|t%eNj42(k1Gh{s%S|a;_yF=_j4}Kv-obGAzaFcLKIZ{t9S|&K}k5_YgK%S zd?DT~$zgN?hQ!d%gQ9r-7dXr{4?}#eYN`4Eg1z7jwt%DYY4hwGyI=qx!#A9J!1mNW z_vN;mK_s?9nO4r813UDSv95GVSF#+H_%-g>hYfFSp#HY{8_JO%O1x&&PGL*#{L20sK?09?c9M z++$iuCvyuZwzo1T dg0NTH{sW1&YnG7-HQxXL002ovPDHLkV1g-Vgn$45 literal 0 HcmV?d00001 diff --git a/docs/generating.adoc b/docs/generating.adoc deleted file mode 100644 index 2001be19fd..0000000000 --- a/docs/generating.adoc +++ /dev/null @@ -1,41 +0,0 @@ -[[synthetic-graphs]] -== Generating Artificial Natural Graphs - -[.tss-floatleft.tss-width-125] -image:splash-graph.png[] - -Real-world graphs are not http://en.wikipedia.org/wiki/Random_graph[random]. Example real-world graphs include social graphs, word graphs, neural graphs, airline graphs, water flow graphs, etc. Interestingly enough, there is a simple statistical understanding of most natural graphs. In short, many vertices have few connections and very few vertices have many connections. A popular algorithm to generate a graph that has this connectivity pattern is known as the http://en.wikipedia.org/wiki/Preferential_attachment[preferential attachment] algorithm which can be simply described with the colloquial phrase: "the rich get richer." This section provides some simple code to artificially generate a natural looking graph in JanusGraph using http://tinkerpop.apache.org/[Gremlin]. - -=== Generating a Graph with Natural Statistics - -The first thing to do is to connect to a Cassandra cluster with JanusGraph. In the example below, a local connection is used where `storage.batch-loading` ensures more speedy performance. - -[source, gremlin] -conf = new BaseConfiguration() -conf.setProperty('storage.backend', 'cassandra') -conf.setProperty('storage.hostname', '127.0.0.1') -conf.setProperty('storage.batch-loading', 'true') -graph = JanusGraphFactory.open(conf) -g = graph.traversal() - -Next, the following script generates a graph with `size` number of edges. - -[source, gremlin] -mgmt = graph.openManagement() -follow = mgmt.makeEdgeLabel('linked').multiplicity(MULTI).make() -mgmt.commit() -size = 100000; ids = [g.addVertex().id()]; rand = new Random() -(1..size).each { - v = graph.addVertex() - u = g.V(ids.get(rand.nextInt(ids.size()))).next() - v.addEdge('linked', u) - ids.add(u.id()) - ids.add(v.id()) - if (it % 1000 == 0) - graph.tx().commit() -} - -=== Computing the In-Degree Distribution of the Graph - -[source, gremlin] -g.V().local(inE().count()).groupCount().order(local).by(valueDecr) diff --git a/docs/static/images/graph-of-the-gods-2.png b/docs/graph-of-the-gods-2.png similarity index 100% rename from docs/static/images/graph-of-the-gods-2.png rename to docs/graph-of-the-gods-2.png diff --git a/docs/hbase.adoc b/docs/hbase.adoc deleted file mode 100644 index 767b2e11bd..0000000000 --- a/docs/hbase.adoc +++ /dev/null @@ -1,204 +0,0 @@ -[[hbase]] -== Apache HBase - -[.tss-center.tss-width-250] -image:http://hbase.apache.org/images/hbase_logo.png[link="http://hbase.apache.org"] - -[quote, 'http://hbase.apache.org/[Apache HBase Homepage]'] -Apache HBase is an open-source, distributed, versioned, non-relational database modeled after http://static.googleusercontent.com/media/research.google.com/en/us/archive/bigtable-osdi06.pdf[Google's Bigtable: A Distributed Storage System for Structured Data by Chang et al.] Just as Bigtable leverages the distributed data storage provided by the Google File System, Apache HBase provides Bigtable-like capabilities on top of Hadoop and HDFS. - -=== HBase Setup - -The following sections outline the various ways in which JanusGraph can be used in concert with Apache HBase. - -==== Local Server Mode - -image:modes-local.png[] - -HBase can be run as a standalone database on the same local host as JanusGraph and the end-user application. In this model, JanusGraph and HBase communicate with one another via a `localhost` socket. Running JanusGraph over HBase requires the following setup steps: - -* Download and extract a stable HBase from http://www.apache.org/dyn/closer.cgi/hbase/stable/. -* Start HBase by invoking the `start-hbase.sh` script in the _bin_ directory inside the extracted HBase directory. To stop HBase, use `stop-hbase.sh`. - -[source, bourne] -$ ./bin/start-hbase.sh -starting master, logging to ../logs/hbase-master-machine-name.local.out - -Now, you can create an HBase JanusGraph as follows: - -[source, java] -JanusGraph graph = JanusGraphFactory.build() - .set("storage.backend", "hbase") - .open(); - -Note, that you do not need to specify a hostname since a localhost connection is attempted by default. Also, in the Gremlin Console, you can not define the type of the variables `conf` and `g`. Therefore, simply leave off the type declaration. - -==== Remote Server Mode - -image:modes-distributed.png[] - -When the graph needs to scale beyond the confines of a single machine, then HBase and JanusGraph are logically separated into different machines. In this model, the HBase cluster maintains the graph representation and any number of JanusGraph instances maintain socket-based read/write access to the HBase cluster. The end-user application can directly interact with JanusGraph within the same JVM as JanusGraph. - -For example, suppose we have a running HBase cluster with a ZooKeeper quorum composed of three machines at IP address 77.77.77.77, 77.77.77.78, and 77.77.77.79, then connecting JanusGraph with the cluster is accomplished as follows: - -[source, java] -JanusGraph g = JanusGraphFactory.build() - .set("storage.backend", "hbase") - .set("storage.hostname", "77.77.77.77, 77.77.77.78, 77.77.77.79") - .open(); - -`storage.hostname` accepts a comma separated list of IP addresses and hostname for any subset of machines in the HBase cluster JanusGraph should connect to. Also, in the Gremlin Console, you can not define the type of the variables `conf` and `g`. Therefore, simply leave off the type declaration. - -==== Remote Server Mode with Gremlin Server - -image:modes-rexster.png[] - -Finally, Gremlin Server can be wrapped around each JanusGraph instance defined in the previous subsection. In this way, the end-user application need not be a Java-based application as it can communicate with Gremlin Server as a client. This type of deployment is great for polyglot architectures where various components written in different languages need to reference and compute on the graph. - ----- -http://gremlin-server.janusgraph.machine1/mygraph/vertices/1 -http://gremlin-server.janusgraph.machine2/mygraph/tp/gremlin?script=g.v(1).out('follows').out('created') ----- - -In this case, each Gremlin Server would be configured to connect to the HBase cluster. The following shows the graph specific fragment of the Gremlin Server configuration. Refer to <> for a complete example and more information on how to configure the server. - -[source, yaml] ----- -... -graphs: { - g: conf/janusgraph-hbase.properties} -plugins: - - janusgraph.imports -... ----- - -=== HBase Specific Configuration - -Refer to <> for a complete listing of all HBase specific configuration options in addition to the general JanusGraph configuration options. - -When configuring HBase it is recommended to consider the following HBase specific configuration options: - -* *storage.hbase.table*: Name of the HBase table in which to store the JanusGraph graph. Allows multiple JanusGraph graphs to co-exist in the same HBase cluster. - -Please refer to the http://hbase.apache.org/book/config.files.html[HBase configuration documentation] for more HBase configuration options and their description. By prefixing the respective HBase configuration option with `storage.hbase.ext` in the JanusGraph configuration it will be passed on to HBase at initialization time. For example, to use the znode /hbase-secure for HBase, set the property: `storage.hbase.ext.zookeeper.znode.parent=/hbase-secure`. The prefix allows arbitrary HBase configuration options to be configured through JanusGraph. - -[IMPORTANT] -HBase backend uses millisecond for timestamps. In JanusGraph 0.2.0 and earlier, if the `graph.timestamps` property is not explicitly set, the default is `MICRO`. -In this case, the `graph.timestamps` property must be explicitly set to `MILLI`. Do not set the `graph.timestamps` property to another value in any cases. - -=== Global Graph Operations - -JanusGraph over HBase supports global vertex and edge iteration. However, note that all these vertices and/or edges will be loaded into memory which can cause `OutOfMemoryException`. Use <> to iterate over all vertices or edges in large graphs effectively. - -=== Deploying on Amazon EC2 - -[quote, 'http://aws.amazon.com/ec2/[Amazon EC2]'] -Amazon EC2 is a web service that provides resizable compute capacity in the cloud. It is designed to make web-scale computing easier for developers. - -Follow these steps to setup an HBase cluster on EC2 and deploy JanusGraph over HBase. To follow these instructions, you need an Amazon AWS account with established authentication credentials and some basic knowledge of AWS and EC2. - -The following commands first launch a four-node HBase cluster on EC2 via http://whirr.apache.org/[Whirr], then run a basic JanusGraph test case using the cluster. - -The configuration described below puts one HBase master server in charge of three HBase regionservers. The master will be the sole member of the Zookeeper quorum by which JanusGraph connects to HBase. - -Whirr 0.7.1 sometimes fails when run on a machine behind a NAT https://issues.apache.org/jira/browse/WHIRR-459[WHIRR-459]. For this reason, it's recommended to use at least Whirr 0.7.2. Whirr 0.8.0 was used to test the following commands on a t1.micro instance running Amazon Linux 2012.03. These commands might need tweaking to produce the intended results on environments besides a t1.micro instance running Amazon Linux 2012.03. - -[source, bourne] ----- -# These commands were executed on a t1.micro instance running Amazon Linux 2012.03 x86_64. -# The AMI identifier for Amazon Linux 2012.03 x86_64 is ami-aecd60c7. -# https://console.aws.amazon.com/ec2/home?region=us-east-1#launchAmi=ami-aecd60c7 -export AWS_ACCESS_KEY_ID=... # Set your Access Key here -export AWS_SECRET_ACCESS_KEY=... # Set your Secret Key here -curl -O http://www.apache.org/dist/whirr/whirr-0.8.0/whirr-0.8.0.tar.gz -tar -xzf whirr-0.8.0.tar.gz && cd whirr-0.8.0 -# Generate an SSH keypair with which Whirr will deploy and manage instances -ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa_whirr -# Download a Whirr recipe for deploying HBase 0.94.1 with hadoop-core 1.0.3 -pushd recipes && wget 'https://raw.github.com/JanusGraph/janusgraph/master/config/whirr-hbase.properties' ; popd -bin/whirr launch-cluster --config recipes/whirr-hbase.properties --private-key-file ~/.ssh/id_rsa_whirr -# Run a superficial health check on the hbase-master node (this should print "imok") -echo "ruok" | nc $(awk '{print $3}' ~/.whirr/hbase-testing/instances | head -1) 2181; echo -# Login to the HBase master node to run the remaining commands -ssh -i ~/.ssh/id_rsa_whirr -o "UserKnownHostsFile /dev/null" \ - -o StrictHostKeyChecking=no \ - `grep hbase-master ~/.whirr/hbase-testing/instances \ - | awk '{print $3}'` -# Maven 2 is available through the package manager, but an incompatibility -# with surefire 2.12 makes it a pain to use; here we download Maven 3 without -# the OS package manager -wget 'http://archive.apache.org/dist/maven/maven-3/3.0.4/binaries/apache-maven-3.0.4-bin.tar.gz' -tar -xzf apache-maven-3.0.4-bin.tar.gz -# Install git -sudo apt-get install -y git-core -# Clone JanusGraph -git clone 'git://github.com/JanusGraph/janusgraph.git' && cd janusgraph -# Run a HBase-backed test of JanusGraph -# -# This test should produce pages of output ending in something like this: -# -# ------------------------------------------------------- -# T E S T S -# ------------------------------------------------------- -# Running org.janusgraph.graphdb.hbase.ExternalHBaseGraphPerformanceTest -# Starting trial 1/1 -# 10000 -# 20000 -# 30000 -# 40000 -# 50000 -# Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 303.659 sec -# -# Results : -# -# Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -# -# [INFO] ------------------------------------------------------------------------ -# [INFO] BUILD SUCCESS -# [INFO] ------------------------------------------------------------------------ -~/apache-maven-3.0.4/bin/mvn test -Dtest=ExternalHBaseGraphPerformanceTest#unlabeledEdgeInsertion -# Check on hadoop -hadoop version # Should print 1.0.3 -# List the hadoop root; should print something like: -# -# Found 4 items -# drwxr-xr-x - hadoop supergroup 0 2012-09-20 00:20 /hadoop -# drwxr-xr-x - hadoop supergroup 0 2012-09-20 00:42 /hbase -# drwxrwxrwx - hadoop supergroup 0 2012-09-20 00:20 /tmp -# drwxrwxrwx - hadoop supergroup 0 2012-09-20 00:20 /user -hadoop fs -ls / ----- - -=== Tips and Tricks for Managing an HBase Cluster - -The http://wiki.apache.org/hadoop/Hbase/Shell[HBase shell] on the master server can be used to get an overall status check of the cluster. - -[source, bourne] -$HBASE_HOME/bin/hbase shell - -From the shell, the following commands are generally useful for understanding the status of the cluster. - -[source, ruby] -status 'janusgraph' -status 'simple' -status 'detailed' - -The above commands can identify if a region server has gone down. If so, it is possible to `ssh` into the failed region server machines and do the following: - -[source, bourne] -sudo -u hadoop $HBASE_HOME/bin/hbase-daemon.sh stop regionserver -sudo -u hadoop $HBASE_HOME/bin/hbase-daemon.sh start regionserver - -The use of http://code.google.com/p/parallel-ssh/[pssh] can make this process easy as there is no need to log into each machine individually to run the commands. Put the IP addresses of the regionservers into a `hosts.txt` file and then execute the following. - -[source, bourne] -pssh -h host.txt sudo -u hadoop $HBASE_HOME/bin/hbase-daemon.sh stop regionserver -pssh -h host.txt sudo -u hadoop $HBASE_HOME/bin/hbase-daemon.sh start regionserver - -Next, sometimes you need to restart the master server (e.g. connection refused exceptions). To do so, on the master execute the following: - -[source, bourne] -sudo -u hadoop $HBASE_HOME/bin/hbase-daemon.sh stop master -sudo -u hadoop $HBASE_HOME/bin/hbase-daemon.sh start master - -Finally, if an HBase cluster has already been deployed and more memory is required of the master or region servers, simply edit the `$HBASE_HOME/conf/hbase-env.sh` files on the respective machines with requisite `-Xmx -Xms` parameters. Once edited, stop/start the master and/or region servers as described previous. diff --git a/docs/images/logos/celum.png b/docs/images/logos/celum.png new file mode 100644 index 0000000000000000000000000000000000000000..344ed040394e4ba9b8f57f3efb33060237b6ab19 GIT binary patch literal 10562 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 LQc3>5r7H3}zi literal 0 HcmV?d00001 diff --git a/docs/images/logos/compose.png b/docs/images/logos/compose.png new file mode 100644 index 0000000000000000000000000000000000000000..769024e514cc29acc329f945062a4fa9ad8b8ecf GIT binary patch literal 57915 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ð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

=8%`#zVz!?TUFwH@e?BRnvy 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 Hlc4_tO%khb literal 0 HcmV?d00001 diff --git a/docs/images/logos/qihoo_360.png b/docs/images/logos/qihoo_360.png new file mode 100644 index 0000000000000000000000000000000000000000..8a3b9e9072416dd4a686ac37317f70437b32485d GIT binary patch literal 5941 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

HoPP^?#~7{Xawgy|VXzUBcUo*ZD$Z a47U##wE}hg?(k2!{^yU1(iM`%0sjppI={*Q literal 0 HcmV?d00001 diff --git a/docs/index-backend/direct-index-query.md b/docs/index-backend/direct-index-query.md new file mode 100644 index 0000000000..c2124e8c7d --- /dev/null +++ b/docs/index-backend/direct-index-query.md @@ -0,0 +1,150 @@ +Direct Index Query +================== + +JanusGraph’s standard global graph querying mechanism supports boolean +queries for vertices or edges. In other words, an element either matches +the query or it does not. There are no partial matches or result +scoring. + +Some indexing backends additionally support fuzzy search queries. For +those queries, a score is computed for each match to indicate the +"goodness" of the match and results are returned in the order of their +score. Fuzzy search is particularly useful when dealing with full-text +search queries where matching more words is considered to be better. + +Since fuzzy search implementations and scoring algorithms differ +significantly between indexing backends, JanusGraph does not support +fuzzy search natively. However, JanusGraph provides a *direct index +query* mechanism that allows search queries to be directly send to the +indexing backend for evaluation (for those backends that support it). + +Use `Graph.indexQuery()` to compose a query that is executed directly +against an indexing backend. This query builder expects two parameters: + +1. The name of the indexing backend to query. This must be the name + configured in JanusGraph’s configuration and used in the property + key indexing definitions + +2. The query string + +The builder allows configuration of the maximum number of elements to be +returned via its `limit(int)` method. The builder’s `offset(int)` +controls number of initial matches in the result set to skip. To +retrieve all vertex or edges matching the given query in the specified +indexing backend, invoke `vertices()` or `edges()`, respectively. It is +not possible to query for both vertices and edges at the same time. +These methods return an `Iterable` over `Result` objects. A result +object contains the matched handle, retrievable via `getElement()`, and +the associated score - `getScore()`. + +Consider the following example: +```java +ManagementSystem mgmt = graph.openManagement(); +PropertyKey text = mgmt.makePropertyKey("text").dataType(String.class).make(); +mgmt.buildIndex("vertexByText", Vertex.class).addKey(text).buildMixedIndex("search"); +mgmt.commit(); +// ... Load vertices ... +for (Result result : graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertices()) { + System.out.println(result.getElement() + ": " + result.getScore()); +} +``` + +Query String +------------ + +The query string is handed directly to the indexing backend for +processing and hence the query string syntax depends on what is +supported by the indexing backend. For vertex queries, JanusGraph will +analyze the query string for property key references starting with "v." +and replace those by a handle to the indexing field that corresponds to +the property key. Likewise, for edge queries, JanusGraph will replace +property key references starting with "e.". Hence, to refer to a +property of a vertex, use "v.\[KEY\_NAME\]" in the query string. +Likewise, for edges write "e.\[KEY\_NAME\]". + +[Elasticsearch](elasticsearch.md) and [Lucene](lucene.md) support the +[Lucene query syntax](http://lucene.apache.org/core/4_10_4/queryparser/org/apache/lucene/queryparser/classic/package-summary.html). +Refer to the [Lucene documentation](http://lucene.apache.org/core/4_1_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html) +or the [Elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) +for more information. The query used in the example above follows the +Lucene query syntax. + + graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertices() + +This query matches all vertices where the text contains any of the three +words (grouped by parentheses) and score matches higher the more words +are matched in the text. + +In addition [Elasticsearch](elasticsearch.md) supports wildcard queries, +use "v.\*" or "e.\*" in the query string to query if any of the +properties on the element match. + +Query Totals +------------ + +It is sometimes useful to know how many total results were returned from +a query without having to retrieve all results. Fortunately, +Elasticsearch and Solr provide a shortcut that does not involve +retrieving and ranking all documents. This shortcut is exposed through +the ".vertexTotals()", ".edgeTotals()", and ".propertyTotals()" methods. + +The totals can be retrieved using the same query syntax as the +indexQuery builder, but size is overwritten to be 0. +```groovy +graph.indexQuery("vertexByText", "v.text:(farm uncle berry)").vertexTotals() +``` + +Gotchas +------- + +### Property Key Names + +Names of property keys that contain non-alphanumeric characters must be +placed in quotation marks to ensure that the query is parsed correctly. +```groovy +graph.indexQuery("vertexByText", "v.\"first_name\":john").vertices() +``` + +Some property key names may be transformed by the JanusGraph indexing +backend implementation. For instance, an indexing backend that does not +permit spaces in field names may transform "My Field Name" to +"My•Field•Name", or an indexing backend like Solr may append type +information to the name, transforming "myBooleanField" to +"myBooleanField\_b". These transformations happen in the index backend’s +implementation of IndexProvider, in the "mapKey2Field" method. Indexing +backends may reserve special characters (such as *•*) and prohibit +indexing of fields that contain them. For this reason it is recommended +to avoid spaces and special characters in property names. + +In general, making direct index queries depends on implementation +details of JanusGraph indexing backends that are normally hidden from +users, so it’s best to verify a query empirically against the indexing +backend in use. + +### Element Identifier Collision + +The strings "v.", "e.", and "p." are used to identify a vertex, edge or +property element respectively in a query. If the field name or the query +value contains the same sequence of characters, this can cause a +collision in the query string and parsing errors as in the following +example: +```groovy +graph.indexQuery("vertexByText", "v.name:v.john").vertices() //DOES NOT WORK! +``` + +To avoid such identifier collisions, use the `setElementIdentifier` +method to define a unique element identifier string that does not occur +in any other parts of the query: +```groovy +graph.indexQuery("vertexByText", "$v$name:v.john").setElementIdentifier("$v$").vertices() +``` + +### Mixed Index Availability Delay + +When a query traverses a [mixed index](../basics/index-performance.md#mixed-index) immediately after +data is inserted the changes may not be visible. In +[Elasticsearch](elasticsearch.md) the configuration option that determines +this delay is [index refresh +interval](https://www.elastic.co/guide/en/elasticsearch/reference/5.4/index-modules.html#dynamic-index-settings). +In [Solr](solr.md) the primary configuration option is [max +time](https://lucene.apache.org/solr/guide/6_6/near-real-time-searching.html). diff --git a/docs/index-backend/elasticsearch.md b/docs/index-backend/elasticsearch.md new file mode 100644 index 0000000000..1b9168a489 --- /dev/null +++ b/docs/index-backend/elasticsearch.md @@ -0,0 +1,263 @@ +Elasticsearch +============= + +> Elasticsearch is a distributed, RESTful search and analytics engine +> capable of solving a growing number of use cases. As the heart of the +> Elastic Stack, it centrally stores your data so you can discover the +> expected and uncover the unexpected. +> +> — [Elasticsearch +> Overview](https://www.elastic.co/products/elasticsearch/) + +JanusGraph supports [Elasticsearch](https://www.elastic.co/) as an index +backend. Here are some of the Elasticsearch features supported by +JanusGraph: + +- **Full-Text**: Supports all `Text` predicates to search for text + properties that matches a given word, prefix or regular expression. +- **Geo**: Supports all `Geo` predicates to search for geo properties + that are intersecting, within, disjoint to or contained in a given + query geometry. Supports points, circles, boxes, lines and polygons + for indexing. Supports circles, boxes and polygons for querying + point properties and all shapes for querying non-point properties. +- **Numeric Range**: Supports all numeric comparisons in `Compare`. +- **Flexible Configuration**: Supports remote operation and open-ended + settings customization. +- **Collections**: Supports indexing SET and LIST cardinality + properties. +- **Temporal**: Nanosecond granularity temporal indexing. +- **Custom Analyzer**: Choose to use a custom analyzer + +Please see [Version Compatibility](../changelog.md#version-compatibility) for details on what versions of +Elasticsearch will work with JanusGraph. + +!!! important + Beginning with Elasticsearch 5.0 JanusGraph uses sandboxed [Painless scripts](https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-painless.html) + for inline updates, which are enabled by default in Elasticsearch 5.x. + + Using JanusGraph with Elasticsearch 2.x requires enabling Groovy + inline scripting by setting `script.engine.groovy.inline.update` to + `true` on the Elasticsearch cluster (see [dynamic scripting documentation](https://www.elastic.co/guide/en/elasticsearch/reference/2.3/modules-scripting.html#enable-dynamic-scripting) + for more information). + +Running Elasticsearch +--------------------- + +JanusGraph supports connections to a running Elasticsearch cluster. +JanusGraph provides two options for running local Elasticsearch +instances for getting started quickly. JanusGraph server (see +[Getting started](../basics/server.md#getting-started)) automatically starts a local +Elasticsearch instance. Alternatively JanusGraph releases include a full +Elasticsearch distribution to allow users to manually start a local +Elasticsearch instance (see [this +page](https://www.elastic.co/guide/en/elasticsearch/guide/current/running-elasticsearch.html) +for more information). + +``` +$ elasticsearch/bin/elasticsearch +``` + +!!! note + For security reasons Elasticsearch must be run under a non-root + account + +Elasticsearch Configuration Overview +------------------------------------ + +JanusGraph supports HTTP(S) client connections to a running +Elasticsearch cluster. Please see [Version Compatibility](../appendices.md#version-compatibility) for details on +what versions of Elasticsearch will work with the different client types +in JanusGraph. + +!!! note + JanusGraph’s index options start with the string "`index.[X].`" where + "`[X]`" is a user-defined name for the backend. This user-defined name + must be passed to JanusGraph’s ManagementSystem interface when + building a mixed index, as described in [Mixed Index](../basics/index-performance.md#mixed-index), so that + JanusGraph knows which of potentially multiple configured index + backends to use. Configuration snippets in this chapter use the name + `search`, whereas prose discussion of options typically write `[X]` in + the same position. The exact index name is not significant as long as + it is used consistently in JanusGraph’s configuration and when + administering indices. + +!!! tip + It’s recommended that index names contain only alphanumeric lowercase + characters and hyphens, and that they start with a lowercase letter. + +### Connecting to Elasticsearch + +The Elasticsearch client is specified as follows: +```conf +index.search.backend=elasticsearch +``` + +When connecting to Elasticsearch a single or list of hostnames for the +Elasticsearch instances must be provided. These are supplied via +JanusGraph’s `index.[X].hostname` key. +```conf +index.search.backend=elasticsearch +index.search.hostname=10.0.0.10:9200 +``` + +Each host or host:port pair specified here will be added to the HTTP +client’s round-robin list of request targets. Here’s a minimal +configuration that will round-robin over 10.0.0.10 on the default +Elasticsearch HTTP port (9200) and 10.0.0.20 on port 7777: +```conf +index.search.backend=elasticsearch +index.search.hostname=10.0.0.10, 10.0.0.20:7777 +``` + +#### JanusGraph `index.[X]` and `index.[X].elasticsearch` options + +JanusGraph only uses default values for `index-name` and +`health-request-timeout`. See [Configuration Reference](../basics/configuration-reference.md) for descriptions of +these options and their accepted values. + +- `index.[X].elasticsearch.index-name` +- `index.[X].elasticsearch.health-request-timeout` + +### REST Client Options + +The REST client accepts the `index.[X].bulk-refresh` option. This option +controls when changes are made visible to search. See [?refresh documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html) +for more information. + +### Ingest Pipelines + +If using Elasticsearch 5.0 or higher, a different ingest pipelines can +be set for each mixed index. Ingest pipeline can be use to pre-process +documents before indexing. A pipeline is composed by a series of +processors. Each processor transforms the document in some way. For +example [date processor](https://www.elastic.co/guide/en/elasticsearch/reference/current/date-processor.html) +can extract a date from a text to a date field. So you can query this +date with JanusGraph without it being physically in the primary storage. + +- `index.[X].elasticsearch.ingest-pipeline.[mixedIndexName] = pipeline_id` + +See [ingest documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html) +for more information about ingest pipelines and [processors documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-processors.html) +for more information about ingest processors. + +Secure Elasticsearch +-------------------- + +Elasticsearch does not perform authentication or authorization. A client +that can connect to Elasticsearch is trusted by Elasticsearch. When +Elasticsearch runs on an unsecured or public network, particularly the +Internet, it should be deployed with some type of external security. +This is generally done with a combination of firewalling, tunneling of +Elasticsearch’s ports or by using Elasticsearch extensions such as +[X-Pack](https://www.elastic.co/guide/en/x-pack/current/index.html). +Elasticsearch has two client-facing ports to consider: + +- The HTTP REST API, usually on port 9200 +- The native "transport" protocol, usually on port 9300 + +A client uses either one protocol/port or the other, but not both +simultaneously. Securing the HTTP protocol port is generally done with a +combination of firewalling and a reverse proxy with SSL encryption and +HTTP authentication. There are a couple of ways to approach security on +the native "transport" protocol port: + +Tunnel Elasticsearch’s native "transport" protocol +This approach can be implemented with SSL/TLS tunneling (for instance +via [stunnel](https://www.stunnel.org/index.html)), a VPN, or SSH port +forwarding. SSL/TLS tunnels require non-trivial setup and monitoring: +one or both ends of the tunnel need a certificate, and the stunnel +processes need to be configured and running continuously. The setup for +most secure VPNs is likewise non-trivial. Some Elasticsearch service +providers handle server-side tunnel management and provide a custom +Elasticsearch `transport.type` to simplify the client setup. + +Add a firewall rule that allows only trusted clients to connect on Elasticsearch’s native protocol port +This is typically done at the host firewall level. Easy to configure, +but very weak security by itself. + +Index Creation Options +---------------------- + +JanusGraph supports customization of the index settings it uses when +creating its Elasticsearch index. It allows setting arbitrary key-value +pairs on the `settings` object in the [Elasticsearch `create index` +request](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html) +issued by JanusGraph. Here is a non-exhaustive sample of Elasticsearch +index settings that can be customized using this mechanism: + +- `index.number_of_replicas` +- `index.number_of_shards` +- `index.refresh_interval` + +Settings customized through this mechanism are only applied when +JanusGraph attempts to create its index in Elasticsearch. If JanusGraph +finds that its index already exists, then it does not attempt to +recreate it, and these settings have no effect. + +### Embedding Elasticsearch index creation settings with `create.ext` + +JanusGraph iterates over all properties prefixed with +`index.[X].elasticsearch.create.ext.`, where `[X]` is an index name such +as `search`. It strips the prefix from each property key. The remainder +of the stripped key will be interpreted as an Elasticsearch index +creation setting. The value associated with the key is not modified. The +stripped key and unmodified value are passed as part of the `settings` +object in the Elasticsearch create index request that JanusGraph issues +when bootstrapping on Elasticsearch. This allows embedding arbitrary +index creation settings settings in JanusGraph’s properties. Here’s an +example configuration fragment that customizes three Elasticsearch index +settings using the `create.ext` config mechanism: +```conf +index.search.backend=elasticsearch +index.search.elasticsearch.create.ext.number_of_shards=15 +index.search.elasticsearch.create.ext.number_of_replicas=3 +index.search.elasticsearch.create.ext.shard.check_on_startup=true +``` + +The configuration fragment listed above takes advantage of +Elasticsearch’s assumption, implemented server-side, that unqualified +`create index` setting keys have an `index.` prefix. It’s also possible +to spell out the index prefix explicitly. Here’s a JanusGraph config +file functionally equivalent to the one listed above, except that the +`index.` prefix before the index creation settings is explicit: +```conf +index.search.backend=elasticsearch +index.search.elasticsearch.create.ext.index.number_of_shards=15 +index.search.elasticsearch.create.ext.index.number_of_replicas=3 +index.search.elasticsearch.create.ext.index.shard.check_on_startup=false +``` + +!!! tip + The `create.ext` mechanism for specifying index creation settings is + compatible with JanusGraph’s Elasticsearch configuration. + +Troubleshooting +--------------- + +### Connection Issues to remote Elasticsearch cluster + +Check that the Elasticsearch cluster nodes are reachable on the HTTP +protocol port from the JanusGraph nodes. Check the node listen port by +examining the Elasticsearch node configuration logs or using a general +diagnostic utility like `netstat`. Check the JanusGraph configuration. + +Optimizing Elasticsearch +------------------------ + +### Write Optimization + +For [bulk loading](../advanced-topics/bulk-loading.md) or other write-intense applications, +consider increasing Elasticsearch’s refresh interval. Refer to [this +discussion](https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html) +on how to increase the refresh interval and its impact on write +performance. Note, that a higher refresh interval means that it takes a +longer time for graph mutations to be available in the index. + +For additional suggestions on how to increase write performance in +Elasticsearch with detailed instructions, please read [this blog post](http://blog.bugsense.com/post/35580279634/indexing-bigdata-with-elasticsearch). + +### Further Reading + +- Please refer to the [Elasticsearch homepage](https://www.elastic.co) + and available documentation for more information on Elasticsearch + and how to setup an Elasticsearch cluster. diff --git a/docs/index-backend/field-mapping.md b/docs/index-backend/field-mapping.md new file mode 100644 index 0000000000..0097771b96 --- /dev/null +++ b/docs/index-backend/field-mapping.md @@ -0,0 +1,85 @@ +Field Mapping +============= + +Individual Field Mapping +------------------------ + +By default, JanusGraph will encode property keys to generate a unique +field name for the property key in the mixed index. If one wants to +query the mixed index directly in the external index backend can be +difficult to deal with and are illegible. For this use case, the field +name can be explicitly specified through a parameter. +```groovy +mgmt = graph.openManagement() +name = mgmt.makePropertyKey('bookname').dataType(String.class).make() +mgmt.buildIndex('booksBySummary', Vertex.class).addKey(name, Parameter.of('mapped-name', 'bookname')).buildMixedIndex("search") +mgmt.commit() +``` + +With this field mapping defined as a parameter, JanusGraph will use the +same name for the field in the `booksBySummary` index created in the +external index system as for the property key. Note, that it must be +ensured that the given field name is unique in the index. + +Global Field Mapping +-------------------- + +Instead of individually adjusting the field mapping for every key added +to a mixed index, one can instruct JanusGraph to always set the field +name in the external index to be identical to the property key name. +This is accomplished by enabling the configuration option `map-name` +which is configured per indexing backend. If this option is enabled for +a particular indexing backend, then all mixed indexes defined against +said backend will use field names identical to the property key names. + +However, this approach has two limitations: 1) The user has to ensure +that the property key names are valid field names for the indexing +backend and 2) renaming the property key will NOT rename the field name +in the index which can lead to naming collisions that the user has to be +aware of and avoid. + +Note, that individual field mappings as described above can be used to +overwrite the default name for a particular key. + +### Custom Analyzer + +By default, JanusGraph will use the default analyzer from the indexing +backend for properties with Mapping.TEXT, and no analyzer for properties +with Mapping.STRING. If one wants to use another analyzer, it can be +explicitly specified through a parameter : ParameterType.TEXT\_ANALYZER +for Mapping.TEXT and ParameterType.STRING\_ANALYZER for Mapping.STRING. + +#### For Elasticsearch + +The name of the analyzer must be set as parameter value. +```groovy +mgmt = graph.openManagement() +string = mgmt.makePropertyKey('string').dataType(String.class).make() +text = mgmt.makePropertyKey('text').dataType(String.class).make() +textString = mgmt.makePropertyKey('textString').dataType(String.class).make() +mgmt.buildIndex('string', Vertex.class).addKey(string, Mapping.STRING.asParameter(), Parameter.of(ParameterType.STRING_ANALYZER.getName(), 'standard')).buildMixedIndex("search") +mgmt.buildIndex('text', Vertex.class).addKey(text, Mapping.TEXT.asParameter(), Parameter.of(ParameterType.TEXT_ANALYZER.getName(), 'english')).buildMixedIndex("search") +mgmt.buildIndex('textString', Vertex.class).addKey(text, Mapping.TEXTSTRING.asParameter(), Parameter.of(ParameterType.STRING_ANALYZER.getName(), 'standard'), Parameter.of(ParameterType.TEXT_ANALYZER.getName(), 'english')).buildMixedIndex("search") +mgmt.commit() +``` + +With these settings, JanusGraph will use the *standard* analyzer for +property key *string* and the *english* analyzer for property key +*text*. + +#### For Solr + +The class of the tokenizer must be set as parameter value. +```groovy +mgmt = graph.openManagement() +string = mgmt.makePropertyKey('string').dataType(String.class).make() +text = mgmt.makePropertyKey('text').dataType(String.class).make() +mgmt.buildIndex('string', Vertex.class).addKey(string, Mapping.STRING.asParameter(), Parameter.of(ParameterType.STRING_ANALYZER.getName(), 'org.apache.lucene.analysis.standard.StandardTokenizer')).buildMixedIndex("search") +mgmt.buildIndex('text', Vertex.class).addKey(text, Mapping.TEXT.asParameter(), Parameter.of(ParameterType.TEXT_ANALYZER.getName(), 'org.apache.lucene.analysis.core.WhitespaceTokenizer')).buildMixedIndex("search") +mgmt.commit() +``` + +With these settings, JanusGraph will use the *standard* tokenizer for +property key *string* and the *whitespace* tokenizer for property key +*text*. + diff --git a/docs/index-backend/index.md b/docs/index-backend/index.md new file mode 100644 index 0000000000..6825b1ce78 --- /dev/null +++ b/docs/index-backend/index.md @@ -0,0 +1,5 @@ +While JanusGraph's composite graph indexes are natively supported through the primary storage backend, mixed graph indexes require that an indexing backend is configured. Mixed indexes provide support for geo, numeric range, and full-text search. + +The choice of index backend determines which search features are supported, as well as the performance and scalability of the index. JanusGraph currently supports three index backends: [Elasticsearch](elasticsearch.md), [Apache Solr](solr.md) and [Apache Lucene](lucene.md). + +Use [Elasticsearch](elasticsearch.md) or [Apache Solr](solr.md) when there is an expectation that JanusGraph will be distributed across multiple machines. [Apache Lucene](lucene.md) performs better in small scale, single machine applications. It performs better in unit tests, for instance. \ No newline at end of file diff --git a/docs/index-backend/lucene.md b/docs/index-backend/lucene.md new file mode 100644 index 0000000000..c426271b15 --- /dev/null +++ b/docs/index-backend/lucene.md @@ -0,0 +1,70 @@ +Apache Lucene +============= + +> Apache Lucene is a high-performance, full-featured text search engine +> library written entirely in Java. It is a technology suitable for +> nearly any application that requires full-text search, especially +> cross-platform. Apache Lucene is an open source project available for +> free download. +> +> — [Apache Lucene Homepage](http://lucene.apache.org/) + +JanusGraph supports [Apache Lucene](http://lucene.apache.org/) as a +single-machine, embedded index backend. Lucene has a slightly extended +feature set and performs better in small-scale applications compared to +[Elasticsearch](elasticsearch.md), but is limited to single-machine +deployments. + +Lucene Embedded Configuration +----------------------------- + +For single machine deployments, Lucene runs embedded with JanusGraph. +JanusGraph starts and interfaces with Lucene internally. + +To run Lucene embedded, add the following configuration options to the +graph configuration file where `/data/searchindex` specifies the +directory where Lucene should store the index data: +```conf +index.search.backend=lucene +index.search.directory=/data/searchindex +``` + +In the above configuration, the index backend is named `search`. Replace +`search` by a different name to change the name of the index. + +Feature Support +--------------- + +- **Full-Text**: Supports all `Text` predicates to search for text + properties that matches a given word, prefix or regular expression. +- **Geo**: Supports `Geo` predicates to search for geo properties that + are intersecting, within, or contained in a given query geometry. + Supports points, lines and polygons for indexing. Supports circles + and boxes for querying point properties and all shapes for querying + non-point properties. +- **Numeric Range**: Supports all numeric comparisons in `Compare`. +- **Temporal**: Nanosecond granularity temporal indexing. + +Configuration Options +--------------------- + +Refer to [Configuration Reference](../basics/configuration-reference.md) for a complete listing of all Lucene +specific configuration options in addition to the general JanusGraph +configuration options. + +Note, that each of the index backend options needs to be prefixed with +`index.[INDEX-NAME].` where `[INDEX-NAME]` stands for the name of the +index backend. For instance, if the index backend is named *search* then +these configuration options need to be prefixed with `index.search.`. To +configure an index backend named *search* to use Lucene as the index +system, set the following configuration option: +```conf +index.search.backend=lucene +``` + +Further Reading +--------------- + +- Please refer to the [Apache Lucene + homepage](http://lucene.apache.org/) and available documentation for + more information on Lucene. diff --git a/docs/searchpredicates.adoc b/docs/index-backend/search-predicates.md similarity index 77% rename from docs/searchpredicates.adoc rename to docs/index-backend/search-predicates.md index be083f2f71..451d14cdfc 100644 --- a/docs/searchpredicates.adoc +++ b/docs/index-backend/search-predicates.md @@ -1,9 +1,8 @@ -[[search-predicates]] -== Search Predicates and Data Types +# Search Predicates and Data Types This page lists all of the comparison predicates that JanusGraph supports in global graph search and local traversals. -=== Compare Predicate +## Compare Predicate The `Compare` enum specifies the following comparison predicates used for index query construction and used in the examples above: @@ -14,23 +13,23 @@ The `Compare` enum specifies the following comparison predicates used for index * `lt` (less than) * `lte` (less than or equal) -=== Text Predicate +## Text Predicate -The `Text` enum specifies the <> used to query for matching text or string values. We differentiate between two types of predicates: +The `Text` enum specifies the [Text Search](text-search.md) used to query for matching text or string values. We differentiate between two types of predicates: * Text search predicates which match against the individual words inside a text string after it has been tokenized. These predicates are not case sensitive. -** `textContains`: is true if (at least) one word inside the text string matches the query string -** `textContainsPrefix`: is true if (at least) one word inside the text string begins with the query string -** `textContainsRegex`: is true if (at least) one word inside the text string matches the given regular expression -** `textContainsFuzzy`: is true if (at least) one word inside the text string is similar to the query String (based on Levenshtein edit distance) + - `textContains`: is true if (at least) one word inside the text string matches the query string + - `textContainsPrefix`: is true if (at least) one word inside the text string begins with the query string + - `textContainsRegex`: is true if (at least) one word inside the text string matches the given regular expression + - `textContainsFuzzy`: is true if (at least) one word inside the text string is similar to the query String (based on Levenshtein edit distance) * String search predicates which match against the entire string value -** `textPrefix`: if the string value starts with the given query string -** `textRegex`: if the string value matches the given regular expression in its entirety -** `textFuzzy`: if the string value is similar to the given query string (based on Levenshtein edit distance) + - `textPrefix`: if the string value starts with the given query string + - `textRegex`: if the string value matches the given regular expression in its entirety + - `textFuzzy`: if the string value is similar to the given query string (based on Levenshtein edit distance) -See <> for more information about full-text and string search. +See [Text Search](text-search.md) for more information about full-text and string search. -=== Geo Predicate +## Geo Predicate The `Geo` enum specifies geo-location predicates. @@ -39,13 +38,13 @@ The `Geo` enum specifies geo-location predicates. * `geoDisjoint` which holds true if the two geometric objects have no points in common (opposite of `geoIntersect`). * `geoContains` which holds true if one geometric object is contained by the other. -See <> for more information about geo search. +See [Geo Mapping](text-search.md#geo-mapping) for more information about geo search. -=== Query Examples +## Query Examples The following query examples demonstrate some of the predicates on the tutorial graph. -[source, gremlin] +```groovy // 1) Find vertices with the name "hercules" g.V().has("name", "hercules") // 2) Find all vertices with an age greater than 50 @@ -68,10 +67,10 @@ g.E().has("reason", textContainsRegex("br[ez]*s")) g.E().has("reason", textContainsFuzzy("love")) // 5) Find all vertices older than a thousand years and named "saturn" g.V().has("age", gt(1000)).has("name", "saturn") +``` +## Data Type Support -[[mixeddatatypes]] -=== Data Type Support While JanusGraph's composite indexes support any data type that can be stored in JanusGraph, the mixed indexes are limited to the following data types. @@ -89,14 +88,14 @@ While JanusGraph's composite indexes support any data type that can be stored in Additional data types will be supported in the future. -[[geoshape]] -=== Geoshape Data Type + +## Geoshape Data Type The Geoshape data type supports representing a point, circle, box, line, polygon, multi-point, multi-line and multi-polygon. Index backends currently support indexing points, circles, boxes, lines, polygons, multi-point, multi-line, multi-polygon and geometry collection. Geospatial index lookups are only supported via mixed indexes. To construct a Geoshape use the following methods: -[source, java] +```groovy //lat, lng Geoshape.point(37.97, 23.72) //lat, lng, radius in km @@ -124,14 +123,19 @@ Geoshape.geoshape(Geoshape.getGeometryCollectionBuilder() .add(Geoshape.getShapeFactory().lineString().pointXY(119.0, 60.0).pointXY(121.0, 60.0).build()) .add(Geoshape.getShapeFactory().polygon().pointXY(119.0, 59.0).pointXY(121.0, 59.0) .pointXY(121.0, 61.0).pointXY(119.0, 61.0).pointXY(119.0, 59.0)).build()) +``` In addition, when importing a graph via GraphSON the geometry may be represented by GeoJSON: -[source, java] - //string + +```json tab="string" "37.97, 23.72" -//list +``` + +```json tab="list" [37.97, 23.72] -//GeoJSON feature +``` + +```json tab="GeoJSON feature" { "type": "Feature", "geometry": { @@ -142,20 +146,23 @@ In addition, when importing a graph via GraphSON the geometry may be represented "name": "Dinagat Islands" } } -//GeoJSON geometry +``` + +```json tab="GeoJSON geometry" { "type": "Point", "coordinates": [125.6, 10.1] } +``` -link:http://geojson.org/[GeoJSON] may be specified as Point, Circle, LineString or Polygon. Polygons must be closed. +[GeoJSON](http://geojson.org/) may be specified as Point, Circle, LineString or Polygon. Polygons must be closed. Note that unlike the JanusGraph API GeoJSON specifies coordinates as lng lat. -=== Collections -If you are using <> then you can index properties with SET and LIST cardinality. +## Collections +If you are using [Elasticsearch](elasticsearch.md) then you can index properties with SET and LIST cardinality. For instance: -[source, gremlin] +```groovy mgmt = graph.openManagement() nameProperty = mgmt.makePropertyKey("names").dataType(String.class).cardinality(Cardinality.SET).make() mgmt.buildIndex("search", Vertex.class).addKey(nameProperty, Mapping.STRING.asParameter()).buildMixedIndex("search") @@ -168,5 +175,5 @@ graph.tx().commit() //Now query it g.V().has("names", "Bob").count().next() //1 g.V().has("names", "Robert").count().next() //1 - +``` diff --git a/docs/index-backend/solr.md b/docs/index-backend/solr.md new file mode 100644 index 0000000000..47b057e620 --- /dev/null +++ b/docs/index-backend/solr.md @@ -0,0 +1,555 @@ +Apache Solr +=========== + +> Solr is the popular, blazing fast open source enterprise search +> platform from the Apache Lucene project. Solr is a standalone +> enterprise search server with a REST-like API. Solr is highly +> reliable, scalable and fault tolerant, providing distributed indexing, +> replication and load-balanced querying, automated failover and +> recovery, centralized configuration and more. +> +> — [Apache Solr Homepage](http://lucene.apache.org/solr/) + +JanusGraph supports [Apache Solr](http://lucene.apache.org/solr/) as an +index backend. Here are some of the Solr features supported by +JanusGraph: + +- **Full-Text**: Supports all `Text` predicates to search for text + properties that matches a given word, prefix or regular expression. +- **Geo**: Supports all `Geo` predicates to search for geo properties + that are intersecting, within, disjoint to or contained in a given + query geometry. Supports points, lines and polygons for indexing. + Supports circles, boxes and polygons for querying point properties + and all shapes for querying non-point properties. +- **Numeric Range**: Supports all numeric comparisons in `Compare`. +- **TTL**: Supports automatically expiring indexed elements. +- **Temporal**: Millisecond granularity temporal indexing. +- **Custom Analyzer**: Choose to use a custom analyzer + +Please see [Version Compatibility](../appendices.md#version-compatibility) +for details on what versions of Solr will work with JanusGraph. + +Solr Configuration Overview +--------------------------- + +JanusGraph supports Solr running in either a SolrCloud or Solr +Standalone (HTTP) configuration for use with a **mixed index** +(see [Mixed Index](../basics/index-performance.md#mixed-index)). +The desired connection mode is configured via the +parameter `mode` which must be set to either `cloud` or `http`, the +former being the default value. For example, to explicitly specify that +Solr is running in a SolrCloud configuration the following property is +specified as a JanusGraph configuration property: + +```conf +index.search.solr.mode=cloud +``` + +These are some key Solr terms: + +- **Core**: A *single index* on a single machine +- **Configuration**: *solrconfig.xml*, *schema.xml*, and other files + required to define a core. +- **Collection**: A *single logical index* that can span multiple + cores on different machines. +- **Configset**: A shared *configuration* that can be reused by + multiple cores. + +Connecting to SolrCloud +----------------------- + +When connecting to a SolrCloud cluster by setting the `mode` equal to +`cloud`, the Zookeeper URL (and optionally port) must be specified so +that JanusGraph can discover and interact with the Solr cluster. + +```conf +index.search.backend=solr +index.search.solr.mode=cloud +index.search.solr.zookeeper-url=localhost:2181 +``` + +A number of additional configuration options pertaining to the creation +of new collections (which is only supported in SolrCloud operation mode) +can be configured to control sharding behavior among other things. Refer +to the [Configuration Reference](../basics/configuration-reference.md) for a complete listing of those options. + +SolrCloud leverages Zookeeper to coordinate collection and configset +information between the Solr servers. The use of Zookeeper with +SolrCloud provides the opportunity to significantly reduce the amount of +manual configuration required to use Solr as a back end index for +JanusGraph. + +### Configset Configuration + +A configset is required to create a collection. The configset is stored +in Zookeeper to enable access to it across the Solr servers. + +- Each collection can provide its own configset when it is created, so + that each collection may have a different configuration. With this + approach, each collection must be created manually. + +- A shared configset can be uploaded separately to Zookeeper if it + will be reused by multiple collections. With this approach, + JanusGraph can create collections automatically by using the shared + configset. Another benefit is that reusing a configset significantly + reduces the amount of data stored in Zookeeper. + +#### Using an Individual Configset + +In this example, a collection named `verticesByAge` is created manually +using the default JanusGraph configuration for Solr that is found in the +distribution. When the collection is created, the configuration is +uploaded into Zookeeper, using the same collection name `verticesByAge` +for the configset name. Refer to the [Solr Reference Guide](https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-CollectionsandCores) +for available parameters. + +```bash +# create the collection +$SOLR_HOME/bin/solr create -c verticesByAge -d $JANUSGRAPH_HOME/conf/solr +``` + +Define a mixed index using `JanusGraphManagement` and the same +collection name. + +```groovy +mgmt = graph.openManagement() +age = mgmt.makePropertyKey("age").dataType(Integer.class).make() +mgmt.buildIndex("verticesByAge", Vertex.class).addKey(age).buildMixedIndex("search") +mgmt.commit() +``` + +#### Using a Shared Configset + +When using a shared configset, it is most convenient to upload the +configuration first as a one time operation. In this example, a +configset named `janusgraph-configset` is uploaded in to Zookeeper using +the default JanusGraph configuration for Solr that is found in the +distribution. Refer to the [Solr Reference Guide](https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-CollectionsandCores) +for available parameters. + +```bash +# upload the shared configset into Zookeeper +# Solr 5 +$SOLR_HOME/server/scripts/cloud-scripts/zkcli.sh -cmd upconfig -z localhost:2181 \ + -d $JANUSGRAPH_HOME/conf/solr -n janusgraph-configset +# Solr 6 and higher +$SOLR_HOME/bin/solr zk upconfig -d $JANUSGRAPH_HOME/conf/solr -n janusgraph-configset \ + -z localhost:2181 +``` + +When configuring the SolrCloud indexing backend for JanusGraph, make +sure to provide the name of the shared configset using the +`index.search.solr.configset` property. + +```conf +index.search.backend=solr +index.search.solr.mode=cloud +index.search.solr.zookeeper-url=localhost:2181 +index.search.solr.configset=janusgraph-configset +``` + +Define a mixed index using `JanusGraphManagement` and the collection +name. + +```groovy +mgmt = graph.openManagement() +age = mgmt.makePropertyKey("age").dataType(Integer.class).make() +mgmt.buildIndex("verticesByAge", Vertex.class).addKey(age).buildMixedIndex("search") +mgmt.commit() +``` + +Connecting to Solr Standalone (HTTP) +------------------------------------ + +When connecting to Solr Standalone via HTTP by setting the `mode` equal +to `http`, a single or list of URLs for the Solr instances must be +provided. + +```conf +index.search.backend=solr +index.search.solr.mode=http +index.search.solr.http-urls=http://localhost:8983/solr +``` + +Additional configuration options for controlling the maximum number of +connections, connection timeout and transmission compression are +available for the HTTP mode. Refer to the [Configuration Reference](../basics/configuration-reference.md) for a +complete listing of those options. + +### Core Configuration + +Solr Standalone is used for a single instance, and it keeps +configuration information on the file system. A core must be created +manually for each mixed index. + +To create a core, a `core_name` and a `configuration` directory is +required. Refer to the [Solr Reference Guide](https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-CollectionsandCores) +for available parameters. In this example, a core named `verticesByAge` +is created using the default JanusGraph configuration for Solr that is +found in the distribution. + +```bash +$SOLR_HOME/bin/solr create -c verticesByAge -d $JANUSGRAPH_HOME/conf/solr +``` + +Define a mixed index using `JanusGraphManagement` and the same core +name. +```groovy +mgmt = graph.openManagement() +age = mgmt.makePropertyKey("age").dataType(Integer.class).make() +mgmt.buildIndex("verticesByAge", Vertex.class).addKey(age).buildMixedIndex("search") +mgmt.commit() +``` + +Solr Schema Design +------------------ + +### Dynamic Field Definition + +By default, JanusGraph uses Solr’s [Dynamic Fields](https://cwiki.apache.org/confluence/display/solr/Dynamic+Fields) +feature to define the field types for all indexed keys. This requires no +extra configuration when adding property keys to a mixed index backed by +Solr and provides better performance than schemaless mode. + +JanusGraph assumes the following dynamic field tags are defined in the +backing Solr collection’s schema.xml file. Please note that there is +additional xml definition of the following fields required in a solr +schema.xml file in order to use them. Reference the example schema.xml +file provided in the `./conf/solr/schema.xml` directory in a JanusGraph +installation for more information. + +```xml + + + + + + + + + + +``` + +In JanusGraph’s default configuration, property key names do not have to +end with the type-appropriate suffix to take advantage of Solr’s dynamic +field feature. JanusGraph generates the Solr field name from the +property key name by encoding the property key definition’s numeric +identifier and the type-appropriate suffix. This means that JanusGraph +uses synthetic field names with type-appropriate suffixes behind the +scenes, regardless of the property key names defined and used by +application code using JanusGraph. This field name mapping can be +overridden through non-default configuration. That’s described in the +next section. + +### Manual Field Definition + +If the user would rather manually define the field types for each of the +indexed fields in a collection, the configuration option `dyn-fields` +needs to be disabled. It is important that the field for each indexed +property key is defined in the backing Solr schema before the property +key is added to the index. + +In this scenario, it is advisable to enable explicit property key name +to field mapping in order to fix the field names for their explicit +definition. This can be achieved in one of two ways: + +1. Configuring the name of the field by providing a `mapped-name` + parameter when adding the property key to the index. See + [Individual Field Mapping](field-mapping.md#individual-field-mapping) for more information. +2. By enabling the `map-name` configuration option for the Solr index + which will use the property key name as the field name in Solr. See + [Global Field Mapping](field-mapping.md#global-field-mapping) for more information. + +### Schemaless Mode + +JanusGraph can also interact with a SolrCloud cluster that is configured +for [schemaless mode](https://cwiki.apache.org/confluence/display/solr/Schemaless+Mode). +In this scenario, the configuration option `dyn-fields` should be +disabled since Solr will infer the field type from the values and not +the field name. + +Note, however, that schemaless mode is recommended only for prototyping +and initial application development and NOT recommended for production +use. + +Troubleshooting +--------------- + +### Collection Does Not Exist + +The collection (and all of the required configuration files) must be +initialized before a defined index can use the collection. See +[Connecting to SolrCloud](#_connecting_to_solrcloud) for more +information. + +When using SolrCloud, the Zookeeper zkCli.sh command line tool can be +used to inspect the configurations loaded into Zookeeper. Also verify +that the default JanusGraph configuration files are copied to the +correct location under solr and that the directory where the files are +copied is correct. + +### Cannot Find the Specified Configset + +When using SolrCloud, a configset is required to create a mixed index +for JanusGraph. See [Configset Configuration](#configset_configuration) for more +information. + +- If using an individual configset, the collection must be created + manually first. +- If using a shared configset, the configset must be uploaded into + Zookeeper first. + +You can verify that the configset and its configuration files are in +Zookeeper under `/configs`. Refer to the [Solr Reference Guide](https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-ZooKeeperOperations) +for other Zookeeper operations. + +```bash +# verify the configset in Zookeeper +# Solr 5 +$SOLR_HOME/server/scripts/cloud-scripts/zkcli.sh -cmd list -z localhost:2181 +# Solr 6 and higher +$SOLR_HOME/bin/solr zk ls -r /configs/configset-name -z localhost:2181 +``` + +### HTTP Error 404 + +This error may be encountered when using Solr Standalone (HTTP) mode. An +example of the error: + +```xml +20:01:22 ERROR org.janusgraph.diskstorage.solr.SolrIndex - Unable to save documents +to Solr as one of the shape objects stored were not compatible with Solr. +org.apache.solr.client.solrj.impl.HttpSolrClient$RemoteSolrException: Error from server +at http://localhost:8983/solr: Expected mime type application/octet-stream but got text/html. + + + +Error 404 Not Found + +

HTTP ERROR 404

+

Problem accessing /solr/verticesByAge/update. Reason: +

    Not Found

+ + +``` + +Make sure to create the core manually before attempting to store data +into the index. See [Core Configuration](#core-configuration) +for more information. + +### Invalid core or collection name + +The core or collection name is an identifier. It must consist entirely +of periods, underscores, hyphens, and/or alphanumerics, and also it may +not start with a hyphen. + +### Connection Problems + +Irrespective of the operation mode, a Solr instance or a cluster of Solr +instances must be running and accessible from the JanusGraph instance(s) +in order for JanusGraph to use Solr as an indexing backend. Check that +the Solr cluster is running correctly and that it is visible and +accessible over the network (or locally) from the JanusGraph instances. + +### JTS ClassNotFoundException with Geo Data + +Solr relies on Spatial4j for geo processing. Spatial4j declares an +optional dependency on JTS ("JTS Topology Suite"). JTS is required for +some geo field definition and query functionality. If the JTS jar is not +on the Solr daemon’s classpath and a field in schema.xml uses a geo +type, then Solr may throw a ClassNotFoundException on one of the missing +JTS classes. The exception can appear when starting Solr using a +schema.xml file designed to work with JanusGraph, but can also appear +when invoking `CREATE` in the [Solr CoreAdmin API](https://wiki.apache.org/solr/CoreAdmin). The exception appears in +slightly different formats on the client and server sides, although the +root cause is identical. + +Here’s a representative example from a Solr server log: + +```java +ERROR [http-8983-exec-5] 2014-10-07 02:54:06, 665 SolrCoreResourceManager.java (line 344) com/vividsolutions/jts/geom/Geometry +java.lang.NoClassDefFoundError: com/vividsolutions/jts/geom/Geometry + at com.spatial4j.core.context.jts.JtsSpatialContextFactory.newSpatialContext(JtsSpatialContextFactory.java:30) + at com.spatial4j.core.context.SpatialContextFactory.makeSpatialContext(SpatialContextFactory.java:83) + at org.apache.solr.schema.AbstractSpatialFieldType.init(AbstractSpatialFieldType.java:95) + at org.apache.solr.schema.AbstractSpatialPrefixTreeFieldType.init(AbstractSpatialPrefixTreeFieldType.java:43) + at org.apache.solr.schema.SpatialRecursivePrefixTreeFieldType.init(SpatialRecursivePrefixTreeFieldType.java:37) + at org.apache.solr.schema.FieldType.setArgs(FieldType.java:164) + at org.apache.solr.schema.FieldTypePluginLoader.init(FieldTypePluginLoader.java:141) + at org.apache.solr.schema.FieldTypePluginLoader.init(FieldTypePluginLoader.java:43) + at org.apache.solr.util.plugin.AbstractPluginLoader.load(AbstractPluginLoader.java:190) + at org.apache.solr.schema.IndexSchema.readSchema(IndexSchema.java:470) + at com.datastax.bdp.search.solr.CassandraIndexSchema.readSchema(CassandraIndexSchema.java:72) + at org.apache.solr.schema.IndexSchema.(IndexSchema.java:168) + at com.datastax.bdp.search.solr.CassandraIndexSchema.(CassandraIndexSchema.java:54) + at com.datastax.bdp.search.solr.core.CassandraCoreContainer.create(CassandraCoreContainer.java:210) + at com.datastax.bdp.search.solr.core.SolrCoreResourceManager.createCore(SolrCoreResourceManager.java:256) + at com.datastax.bdp.search.solr.handler.admin.CassandraCoreAdminHandler.handleCreateAction(CassandraCoreAdminHandler.java:117) + ... +``` + +Here’s what normally appears in the output of the client that issued the +associated `CREATE` command to the CoreAdmin API: + +```java +org.apache.solr.common.SolrException: com/vividsolutions/jts/geom/Geometry + at com.datastax.bdp.search.solr.core.SolrCoreResourceManager.createCore(SolrCoreResourceManager.java:345) + at com.datastax.bdp.search.solr.handler.admin.CassandraCoreAdminHandler.handleCreateAction(CassandraCoreAdminHandler.java:117) + at org.apache.solr.handler.admin.CoreAdminHandler.handleRequestBody(CoreAdminHandler.java:152) + ... +``` + +This is resolved by adding the JTS jar to the classpath of JanusGraph +and/or the Solr server. JTS is not included in JanusGraph distributions +by default due to its LGPL license. Users must download the [JTS jar file](http://search.maven.org/remotecontent?filepath=com/vividsolutions/jts/1.13/jts-1.13.jar) +separately and copy it into the JanusGraph and/or Solr server lib +directory. If using Solr’s built in web server, the JTS jar may be +copied to the example/solr-webapp/webapp/WEB-INF/lib directory to +include it in the classpath. Solr can be restarted, and the exception +should be gone. Solr must be started once with the correct schema.xml +file in place first, for the example/solr-webapp/webapp/WEB-INF/lib +directory to exist. + +To determine the ideal JTS version for Solr server, first check the +version of Spatial4j in use by the Solr cluster, then determine the +version of JTS against which that Spatial4j version was compiled. +Spatial4j declares its target JTS version in the [pom for the `com.spatial4j:spatial4j` artifact](http://search.maven.org/#search|gav|1|g%3A%22com.spatial4j%22%20AND%20a%3A%22spatial4j%22). +Copy the JTS jar to the server/solr-webapp/webapp/WEB-INF/lib directory +in your solr installation. + +Advanced Solr Configuration +--------------------------- + +### DSE Search + +This section covers installation and configuration of JanusGraph with +DataStax Enterprise (DSE) Search. There are multiple ways to install +DSE, but this section focuses on DSE’s binary tarball install option on +Linux. Most of the steps in this section can be generalized to the other +install options for DSE. + +Install DataStax Enterprise as directed by the page [Installing DataStax Enterprise using the binary tarball](http://www.datastax.com/documentation/datastax_enterprise/4.5/datastax_enterprise/install/installTARdse.html). + +Export `DSE_HOME` and append to `PATH` in your shell environment. Here’s +an example using Bash syntax: +```bash +export DSE_HOME=/path/to/dse-version.number +export PATH="$DSE_HOME"/bin:"$PATH" +``` + +Install JTS for Solr. The appropriate version varies with the Spatial4j +version. As of DSE 4.5.2, the appropriate version is 1.13. +```bash +cd $DSE_HOME/resources/solr/lib +curl -O 'http://central.maven.org/maven2/com/vividsolutions/jts/1.13/jts-1.13.jar' +``` + +Start DSE Cassandra and Solr in a single background daemon: + +```bash +# The "dse-data" path below was chosen to match the +# "Installing DataStax Enterprise using the binary tarball" +# documentation page from DataStax. The exact path is not +# significant. +dse cassandra -s -Ddse.solr.data.dir="$DSE_HOME"/dse-data/solr +``` + +The previous command will write some startup information to the console +and to the logfile path `log4j.appender.R.File` configured in +`$DSE_HOME/resources/cassandra/conf/log4j-server.properties`. + +Once DSE with Cassandra and Solr has started normally, check the cluster +health with `nodetool status`. A single-instance ring should show one +node with flags \*U\*p and \*N\*ormal: + +```bash +nodetool status +Note: Ownership information does not include topology; for complete information, specify a keyspace += Datacenter: Solr +Status=Up/Down +|/ State=Normal/Leaving/Joining/Moving +-- Address Load Owns Host ID Token Rack +UN 127.0.0.1 99.89 KB 100.0% 5484ef7b-ebce-4560-80f0-cbdcd9e9f496 -7317038863489909889 rack1 +``` + +Next, switch to Gremlin Console and open a JanusGraph database against +the DSE instance. This will create JanusGraph’s keyspace and column +families. + +```bash +cd $JANUSGRAPH_HOME +bin/gremlin.sh + + \,,,/ + (o o) +-----oOOo-(3)-oOOo----- +gremlin> graph = JanusGraphFactory.open('conf/janusgraph-cql-solr.properties') +==>janusgraph[cql:[127.0.0.1]] +gremlin> g = graph.traversal() +==>graphtraversalsource[janusgraph[cql:[127.0.0.1]], standard] +gremlin> +``` + +Keep this Gremlin Console open. We’ll take a break now to install a Solr +core. Then we’ll come back to this console to load some sample data. + +Next, upload configuration files for JanusGraph’s Solr collection, then +create the core in DSE: + +```bash +# Change to the directory where JanusGraph was extracted. Later commands +# use relative paths to the Solr config files shipped with the JanusGraph +# distribution. +cd $JANUSGRAPH_HOME + +# The name must be URL safe and should contain one dot/full-stop +# character. The part of the name after the dot must not conflict with +# any of JanusGraph's internal CF names. Starting the part after the dot +# "solr" will avoid a conflict with JanusGraph's internal CF names. +CORE_NAME=janusgraph.solr1 +# Where to upload collection configuration and send CoreAdmin requests. +SOLR_HOST=localhost:8983 + +# The value of index.[X].solr.http-urls in JanusGraph's config file +# should match $SOLR_HOST and $CORE_NAME. For example, given the +# $CORE_NAME and $SOLR_HOST values above, JanusGraph's config file would +# contain (assuming "search" is the desired index alias): +# +# index.search.solr.http-urls=http://localhost:8983/solr/janusgraph.solr1 +# +# The stock JanusGraph config file conf/janusgraph-cql-solr.properties +# ships with this http-urls value. + +# Upload Solr config files to DSE Search daemon +for xml in conf/solr/{solrconfig, schema, elevate}.xml ; do + curl -v http://"$SOLR_HOST"/solr/resource/"$CORE_NAME/$xml" \ + --data-binary @"$xml" -H 'Content-type:text/xml; charset=utf-8' +done +for txt in conf/solr/{protwords, stopwords, synonyms}.txt ; do + curl -v http://"$SOLR_HOST"/solr/resource/"$CORE_NAME/$txt" \ + --data-binary @"$txt" -H 'Content-type:text/plain; charset=utf-8' +done +sleep 5 + +# Create core using the Solr config files just uploaded above +curl "http://"$SOLR_HOST"/solr/admin/cores?action=CREATE&name=$CORE_NAME" +sleep 5 + +# Retrieve and print the status of the core we just created +curl "http://localhost:8983/solr/admin/cores?action=STATUS&core=$CORE_NAME" +``` + +Now the JanusGraph database and backing Solr core are ready for use. We +can test it out with the [Graph of the Gods](../index.md#getting-started) dataset. +Picking up the Gremlin Console session started above: +```groovy +// Assuming graph = JanusGraphFactory.open('conf/janusgraph-cql-solr.properties')... +gremlin> GraphOfTheGodsFactory.load(graph) +==>null +``` + +Now we can run any of the queries described in [Getting started](../index.md#getting-started). +Queries involving text and geo predicates will be served by Solr. For +more verbose reporting from JanusGraph and the Solr client, run +`gremlin.sh -l DEBUG` and issue some index-backed queries. diff --git a/docs/index-backend/text-search.md b/docs/index-backend/text-search.md new file mode 100644 index 0000000000..2d30ad486a --- /dev/null +++ b/docs/index-backend/text-search.md @@ -0,0 +1,165 @@ +Index Parameters and Full-Text Search +===================================== + +When defining a mixed index, a list of parameters can be optionally +specified for each property key added to the index. These parameters +control how the particular key is to be indexed. JanusGraph recognizes +the following index parameters. Whether these are supported depends on +the configured index backend. A particular index backend might also +support custom parameters in addition to the ones listed here. + +Full-Text Search +---------------- + +When indexing string values, that is property keys with `String.class` +data type, one has the choice to either index those as text or character +strings which is controlled by the `mapping` parameter type. + +When the value is indexed as text, the string is tokenized into a bag of +words which allows the user to efficiently query for all matches that +contain one or multiple words. This is commonly referred to as +**full-text search**. When the value is indexed as a character string, +the string is index "as-is" without any further analysis or +tokenization. This facilitates queries looking for an exact character +sequence match. This is commonly referred to as **string search**. + +### Full-Text Search + +By default, strings are indexed as text. To make this indexing option +explicit, one can define a mapping when indexing a property key as text. + +```groovy +mgmt = graph.openManagement() +summary = mgmt.makePropertyKey('booksummary').dataType(String.class).make() +mgmt.buildIndex('booksBySummary', Vertex.class).addKey(summary, Mapping.TEXT.asParameter()).buildMixedIndex("search") +mgmt.commit() +``` + +This is identical to a standard mixed index definition with the only +addition of an extra parameter that specifies the mapping in the index - +in this case `Mapping.TEXT`. + +When a string property is indexed as text, the string value is tokenized +into a bag of tokens. The exact tokenization depends on the indexing +backend and its configuration. JanusGraph’s default tokenization splits +the string on non-alphanumeric characters and removes any tokens with +less than 2 characters. The tokenization used by an indexing backend may +differ (e.g. stop words are removed) which can lead to minor differences +in how full-text search queries are handled for modifications inside a +transaction and committed data in the indexing backend. + +When a string property is indexed as text, only full-text search +predicates are supported in graph queries by the indexing backend. +Full-text search is case-insensitive. + +- `textContains`: is true if (at least) one word inside the text + string matches the query string +- `textContainsPrefix`: is true if (at least) one word inside the text + string begins with the query string +- `textContainsRegex`: is true if (at least) one word inside the text + string matches the given regular expression +- `textContainsFuzzy`: is true if (at least) one word inside the text + string is similar to the query String (based on Levenshtein edit + distance) + +```groovy +import static org.janusgraph.core.attribute.Text.* +g.V().has('booksummary', textContains('unicorns')) +g.V().has('booksummary', textContainsPrefix('uni')) +g.V().has('booksummary', textContainsRegex('.*corn.*')) +g.V().has('booksummary', textContainsFuzzy('unicorn')) +``` + +String search predicates (see below) may be used in queries, but those +require filtering in memory which can be very costly. + +### String Search + +To index string properties as character sequences without any analysis +or tokenization, specify the mapping as `Mapping.STRING`: + +```groovy +mgmt = graph.openManagement() +name = mgmt.makePropertyKey('bookname').dataType(String.class).make() +mgmt.buildIndex('booksBySummary', Vertex.class).addKey(name, Mapping.STRING.asParameter()).buildMixedIndex("search") +mgmt.commit() +``` + +When a string mapping is configured, the string value is indexed and can +be queried "as-is" - including stop words and non-letter characters. +However, in this case the query must match the entire string value. +Hence, the string mapping is useful when indexing short character +sequences that are considered to be one token. + +When a string property is indexed as string, only the following +predicates are supported in graph queries by the indexing backend. +String search is case-sensitive. + +- `eq`: if the string is identical to the query string +- `neq`: if the string is different than the query string +- `textPrefix`: if the string value starts with the given query string +- `textRegex`: if the string value matches the given regular + expression in its entirety +- `textFuzzy`: if the string value is similar to the given query + string (based on Levenshtein edit distance) + +```groovy +import static org.apache.tinkerpop.gremlin.process.traversal.P.* +import static org.janusgraph.core.attribute.Text.* +g.V().has('bookname', eq('unicorns')) +g.V().has('bookname', neq('unicorns')) +g.V().has('bookname', textPrefix('uni')) +g.V().has('bookname', textRegex('.*corn.*')) +g.V().has('bookname', textFuzzy('unicorn')) +``` + +Full-text search predicates may be used in queries, but those require +filtering in memory which can be very costly. + +### Full text and string search + +If you are using Elasticsearch it is possible to index properties as +both text and string allowing you to use all of the predicates for exact +and fuzzy matching. + +```groovy +mgmt = graph.openManagement() +summary = mgmt.makePropertyKey('booksummary').dataType(String.class).make() +mgmt.buildIndex('booksBySummary', Vertex.class).addKey(summary, Mapping.TEXTSTRING.asParameter()).buildMixedIndex("search") +mgmt.commit() +``` + +Note that the data will be stored in the index twice, once for exact +matching and once for fuzzy matching. + +Geo Mapping +----------- + +By default, JanusGraph supports indexing geo properties with point type +and querying geo properties by circle or box. To index a non-point geo +property with support for querying by any geoshape type, specify the +mapping as `Mapping.PREFIX_TREE`: + +```groovy +mgmt = graph.openManagement() +name = mgmt.makePropertyKey('border').dataType(Geoshape.class).make() +mgmt.buildIndex('borderIndex', Vertex.class).addKey(name, Mapping.PREFIX_TREE.asParameter()).buildMixedIndex("search") +mgmt.commit() +``` + +Additional parameters can be specified to tune the configuration of the +underlying prefix tree mapping. These optional parameters include the +number of levels used in the prefix tree as well as the associated +precision. + +```groovy +mgmt = graph.openManagement() +name = mgmt.makePropertyKey('border').dataType(Geoshape.class).make() +mgmt.buildIndex('borderIndex', Vertex.class).addKey(name, Mapping.PREFIX_TREE.asParameter(), Parameter.of("index-geo-max-levels", 18), Parameter.of("index-geo-dist-error-pct", 0.0125)).buildMixedIndex("search") +mgmt.commit() +``` + +Note that some indexing backends (e.g. Solr) may require additional +external schema configuration to support and tune indexing non-point +properties. + diff --git a/docs/index.adoc b/docs/index.adoc deleted file mode 100644 index 4d73f733cd..0000000000 --- a/docs/index.adoc +++ /dev/null @@ -1,21 +0,0 @@ -= JanusGraph Documentation - -include::adoc_attributes.adoc[] - -include::intro.adoc[] - -include::basics.adoc[] - -include::connecting.adoc[] - -include::storagebackends.adoc[] - -include::indexbackends.adoc[] - -include::advanced.adoc[] - -include::internals.adoc[] - -include::development.adoc[] - -include::appendices.adoc[] diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..beb6f6508a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,457 @@ +## The Benefits of JanusGraph + +JanusGraph is designed to support the processing of graphs so large that +they require storage and computational capacities beyond what a single +machine can provide. Scaling graph data processing for real time +traversals and analytical queries is JanusGraph’s foundational benefit. +This section will discuss the various specific benefits of JanusGraph +and its underlying, supported persistence solutions. + +### General JanusGraph Benefits + +- Support for very large graphs. JanusGraph graphs scale with the + number of machines in the cluster. +- Support for very many concurrent transactions and operational graph + processing. JanusGraph’s transactional capacity scales with the + number of machines in the cluster and answers complex traversal + queries on huge graphs in milliseconds. +- Support for global graph analytics and batch graph processing + through the Hadoop framework. +- Support for geo, numeric range, and full text search for vertices + and edges on very large graphs. +- Native support for the popular property graph data model exposed by + [Apache TinkerPop](http://tinkerpop.apache.org/). +- Native support for the graph traversal language + [Gremlin](http://tinkerpop.apache.org/gremlin.html). + - Numerous graph-level configurations provide knobs for tuning + performance. +- Vertex-centric indices provide vertex-level querying to alleviate + issues with the infamous [super node problem](http://thinkaurelius.com/2012/10/25/a-solution-to-the-supernode-problem/). +- Provides an optimized disk representation to allow for efficient use + of storage and speed of access. +- Open source under the liberal [Apache 2 license](http://en.wikipedia.org/wiki/Apache_License). + +### Benefits of JanusGraph with Apache Cassandra + +
+ +
+ +- [Continuously available](http://en.wikipedia.org/wiki/Continuous_availability) + with no single point of failure. +- No read/write bottlenecks to the graph as there is no master/slave + architecture. + +- [Elastic scalability](http://en.wikipedia.org/wiki/Elastic_computing) allows + for the introduction and removal of machines. +- Caching layer ensures that continuously accessed data is available + in memory. +- Increase the size of the cache by adding more machines to the + cluster. +- Integration with [Apache Hadoop](http://hadoop.apache.org/). +- Open source under the liberal Apache 2 license. + +### Benefits of JanusGraph with HBase + +
+ +
+ +- Tight integration with the [Apache Hadoop](http://hadoop.apache.org/) ecosystem. +- Native support for [strong consistency](http://en.wikipedia.org/wiki/Strong_consistency). +- Linear scalability with the addition of more machines. +- [Strictly consistent](http://en.wikipedia.org/wiki/Strict_consistency) reads and writes. +- Convenient base classes for backing Hadoop + [MapReduce](http://en.wikipedia.org/wiki/MapReduce) jobs with HBase + tables. +- Support for exporting metrics via + [JMX](http://en.wikipedia.org/wiki/Java_Management_Extensions). +- Open source under the liberal Apache 2 license. + +### JanusGraph and the CAP Theorem + +> Despite your best efforts, your system will experience enough faults +> that it will have to make a choice between reducing yield (i.e., stop +> answering requests) and reducing harvest (i.e., giving answers based +> on incomplete data). This decision should be based on business +> requirements. +> +> — [Coda Hale](http://codahale.com/you-cant-sacrifice-partition-tolerance) + +When using a database, the [CAP theorem](http://en.wikipedia.org/wiki/CAP_theorem) should be thoroughly +considered (C=Consistency, A=Availability, P=Partitionability). +JanusGraph is distributed with 3 supporting backends: [Apache Cassandra](http://cassandra.apache.org/), + [Apache HBase](http://hbase.apache.org/), and [Oracle Berkeley DB Java Edition](http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html). +Note that BerkeleyDB JE is a non-distributed database and is typically +only used with JanusGraph for testing and exploration purposes. + +HBase gives preference to consistency at the expense of yield, i.e. the +probability of completing a request. Cassandra gives preference to +availability at the expense of harvest, i.e. the completeness of the +answer to the query (data available/complete data). + +## Getting Started +The examples in this section make extensive use of a toy graph +distributed with JanusGraph called *The Graph of the Gods*. This graph +is diagrammed below. The abstract data model is known as a +[Property Graph Model](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference#intro) +and this particular instance describes the relationships between the +beings and places of the Roman pantheon. Moreover, special text and +symbol modifiers in the diagram (e.g. bold, underline, etc.) denote +different schematics/typings in the graph. + +![Graph of the Gods](graph-of-the-gods-2.png) + +| visual symbol | meaning | +| ------------- |-------------| +|bold key|a graph indexed key| +|bold key with star|a graph indexed key that must have a unique value| +|underlined key|a vertex-centric indexed key| +|hollow-head edge|a functional/unique edge (no duplicates)| +|tail-crossed edge|a unidirectional edge (can only traverse in one direction)| + +### Downloading JanusGraph and Running the Gremlin Console + +JanusGraph can be downloaded from the +[Releases](https://github.com/JanusGraph/janusgraph/releases) section of +the project repository. Once retrieved and unpacked, a Gremlin Console +can be opened. The Gremlin Console is a +[REPL](http://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) +(i.e. interactive shell) that is distributed with JanusGraph and only +differs from the standard Gremlin Console insofar that JanusGraph is a +pre-installed and pre-loaded package. Alternatively, a user may choose +to install and activate JanusGraph in an existing Gremlin Console by +downloading the JanusGraph package from the central repository. In the +example below, `janusgraph.zip` is used, however, be sure to unzip the +zip-file that was downloaded. + +!!! important + JanusGraph requires Java 8 (Standard Edition). Oracle Java 8 is + recommended. JanusGraph’s shell scripts expect that the `$JAVA_HOME` + environment variable points to the directory where JRE or JDK is + installed. + +```bash +$ unzip janusgraph-{{ latest_version }}-hadoop2.zip +Archive: janusgraph-{{ latest_version }}-hadoop2.zip + creating: janusgraph-{{ latest_version }}-hadoop2/ +... +$ cd janusgraph-{{ latest_version }}-hadoop2 +$ bin/gremlin.sh + + \,,,/ + (o o) +-----oOOo-(3)-oOOo----- +09:12:24 INFO org.apache.tinkerpop.gremlin.hadoop.structure.HadoopGraph - HADOOP_GREMLIN_LIBS is set to: /usr/local/janusgraph/lib +plugin activated: tinkerpop.hadoop +plugin activated: janusgraph.imports +gremlin> +``` + +The Gremlin Console interprets commands using [Apache Groovy](http://www.groovy-lang.org/). +Groovy is a superset of Java that +has various shorthand notations that make interactive programming +easier. Likewise Gremlin-Groovy is a superset of Groovy with various +shorthand notations that make graph traversals easy. The basic examples +below demonstrate handling numbers, strings, and maps. The remainder of +the tutorial will discuss graph-specific constructs. +```groovy +gremlin> 100-10 +==>90 +gremlin> "JanusGraph:" + " The Rise of Big Graph Data" +==>JanusGraph: The Rise of Big Graph Data +gremlin> [name:'aurelius', vocation:['philosopher', 'emperor']] +==>name=aurelius +==>vocation=[philosopher, emperor] +``` + +!!! tip + Refer to [Apache TinkerPop](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference), + [SQL2Gremlin](http://sql2gremlin.com/), and [Gremlin Recipes](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/recipes/) + for more information about using Gremlin. + +### Loading the Graph of the Gods Into JanusGraph + +The example below will open a JanusGraph graph instance and load *The +Graph of the Gods* dataset diagrammed above. `JanusGraphFactory` +provides a set of static `open` methods, each of which takes a +configuration as its argument and returns a graph instance. This +tutorial calls one of these `open` methods on a configuration that uses +the [BerkeleyDB](storage-backend/bdb.md) storage backend and the +[Elasticsearch](index-backend/elasticsearch.md) index backend, then loads *The Graph of +the Gods* using the helper class `GraphOfTheGodsFactory`. This section +skips over the configuration details, but additional information about +storage backends, index backends, and their configuration are available +in [Storage Backends](storage-backend/index.md), [Index Backends](index-backend/search-predicates.md), and +[Configuration Reference](basics/configuration-reference.md). + +```groovy +gremlin> graph = JanusGraphFactory.open('conf/janusgraph-berkeleyje-es.properties') +==>standardjanusgraph[berkeleyje:../db/berkeley] +gremlin> GraphOfTheGodsFactory.load(graph) +==>null +gremlin> g = graph.traversal() +==>graphtraversalsource[standardjanusgraph[berkeleyje:../db/berkeley], standard] +``` + +The `JanusGraphFactory.open() and GraphOfTheGodsFactory.load()` methods +do the following to the newly constructed graph prior to returning it: + +1. Creates a collection of global and vertex-centric indices on the graph. +2. Adds all the vertices to the graph along with their properties. +3. Adds all the edges to the graph along with their properties. + +Please see the [GraphOfTheGodsFactory source code](https://github.com/JanusGraph/janusgraph/blob/master/janusgraph-core/src/main/java/org/janusgraph/example/GraphOfTheGodsFactory.java) +for details. + +For those using JanusGraph/Cassandra (or JanusGraph/HBase), be sure to +make use of `conf/janusgraph-cql-es.properties` (or +`conf/janusgraph-hbase-es.properties`) and +`GraphOfTheGodsFactory.load()`. + +```groovy +gremlin> graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties') +==>standardjanusgraph[cql:[127.0.0.1]] +gremlin> GraphOfTheGodsFactory.load(graph) +==>null +gremlin> g = graph.traversal() +==>graphtraversalsource[standardjanusgraph[cql:[127.0.0.1]], standard] +``` + +You may also use the `conf/janusgraph-cql.properties`, +`conf/janusgraph-berkeleyje.properties`, or +`conf/janusgraph-hbase.properties` configuration files to open a graph +without an indexing backend configured. In such cases, you will need to +use the `GraphOfTheGodsFactory.loadWithoutMixedIndex()` method to load +the *Graph of the Gods* so that it doesn’t attempt to make use of an +indexing backend. +```groovy +gremlin> graph = JanusGraphFactory.open('conf/janusgraph-cql.properties') +==>standardjanusgraph[cql:[127.0.0.1]] +gremlin> GraphOfTheGodsFactory.loadWithoutMixedIndex(graph, true) +==>null +gremlin> g = graph.traversal() +==>graphtraversalsource[standardjanusgraph[cql:[127.0.0.1]], standard] +``` +### Global Graph Indices + + +The typical pattern for accessing data in a graph database is to first +locate the entry point into the graph using a graph index. That entry +point is an element (or set of elements) — i.e. a vertex or edge. From +the entry elements, a Gremlin path description describes how to traverse +to other elements in the graph via the explicit graph structure. + +Given that there is a unique index on `name` property, the Saturn vertex +can be retrieved. The property map (i.e. the key/value pairs of Saturn) +can then be examined. As demonstrated, the Saturn vertex has a `name` of +"saturn, " an `age` of 10000, and a `type` of "titan." The grandchild of +Saturn can be retrieved with a traversal that expresses: "Who is +Saturn’s grandchild?" (the inverse of "father" is "child"). The result +is Hercules. + +```groovy +gremlin> saturn = g.V().has('name', 'saturn').next() +==>v[256] +gremlin> g.V(saturn).valueMap() +==>[name:[saturn], age:[10000]] +gremlin> g.V(saturn).in('father').in('father').values('name') +==>hercules +``` + +The property `place` is also in a graph index. The property `place` is +an edge property. Therefore, JanusGraph can index edges in a graph +index. It is possible to query *The Graph of the Gods* for all events +that have happened within 50 kilometers of +[Athens](http://en.wikipedia.org/wiki/Athens) (latitude:37.97 and +long:23.72). Then, given that information, which vertices were involved +in those events. + +```groovy +gremlin> g.E().has('place', geoWithin(Geoshape.circle(37.97, 23.72, 50))) +==>e[a9x-co8-9hx-39s][16424-battled->4240] +==>e[9vp-co8-9hx-9ns][16424-battled->12520] +gremlin> g.E().has('place', geoWithin(Geoshape.circle(37.97, 23.72, 50))).as('source').inV().as('god2').select('source').outV().as('god1').select('god1', 'god2').by('name') +==>[god1:hercules, god2:hydra] +==>[god1:hercules, god2:nemean] +``` +Graph indices are one type of index structure in JanusGraph. Graph +indices are automatically chosen by JanusGraph to answer which ask for +all vertices (`g.V`) or all edges (`g.E`) that satisfy one or multiple +constraints (e.g. `has` or `interval`). The second aspect of indexing in +JanusGraph is known as vertex-centric indices. Vertex-centric indices +are utilized to speed up traversals inside the graph. Vertex-centric +indices are described later. + +#### Graph Traversal Examples + +> [Hercules](http://en.wikipedia.org/wiki/Hercules), son of Jupiter and +> [Alcmene](http://en.wikipedia.org/wiki/Alcmene), bore super human +> strength. Hercules was a +> [Demigod](http://en.wikipedia.org/wiki/Demigod) because his father was +> a god and his mother was a human. +> [Juno](http://en.wikipedia.org/wiki/Juno_(mythology)), wife of +> Jupiter, was furious with Jupiter’s infidelity. In revenge, she +> blinded Hercules with temporary insanity and caused him to kill his +> wife and children. To atone for the slaying, Hercules was ordered by +> the [Oracle of Delphi](http://en.wikipedia.org/wiki/Oracle_at_Delphi) +> to serve [Eurystheus](http://en.wikipedia.org/wiki/Eurystheus). +> Eurystheus appointed Hercules to 12 labors. + +In the previous section, it was demonstrated that Saturn’s grandchild +was Hercules. This can be expressed using a `loop`. In essence, Hercules +is the vertex that is 2-steps away from Saturn along the `in('father')` +path. +```groovy +gremlin> hercules = g.V(saturn).repeat(__.in('father')).times(2).next() +==>v[1536] +``` + +Hercules is a demigod. To prove that Hercules is half human and half +god, his parent’s origins must be examined. It is possible to traverse +from the Hercules vertex to his mother and father. Finally, it is +possible to determine the `type` of each of them — yielding "god" and +"human." +```groovy +gremlin> g.V(hercules).out('father', 'mother') +==>v[1024] +==>v[1792] +gremlin> g.V(hercules).out('father', 'mother').values('name') +==>jupiter +==>alcmene +gremlin> g.V(hercules).out('father', 'mother').label() +==>god +==>human +gremlin> hercules.label() +==>demigod +``` + +The examples thus far have been with respect to the genetic lines of the +various actors in the Roman pantheon. The [Property Graph Model](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference#intro) +is expressive enough to represent multiple types of things and +relationships. In this way, *The Graph of the Gods* also identifies +Hercules' various heroic exploits --- his famous 12 labors. In the +previous section, it was discovered that Hercules was involved in two +battles near Athens. It is possible to explore these events by +traversing `battled` edges out of the Hercules vertex. +```groovy +gremlin> g.V(hercules).out('battled') +==>v[2304] +==>v[2560] +==>v[2816] +gremlin> g.V(hercules).out('battled').valueMap() +==>[name:[nemean]] +==>[name:[hydra]] +==>[name:[cerberus]] +gremlin> g.V(hercules).outE('battled').has('time', gt(1)).inV().values('name') +==>cerberus +==>hydra +``` + +The edge property `time` on `battled` edges is indexed by the +vertex-centric indices of a vertex. Retrieving `battled` edges incident +to Hercules according to a constraint/filter on `time` is faster than +doing a linear scan of all edges and filtering (typically `O(log n)`, +where `n` is the number incident edges). JanusGraph is intelligent +enough to use vertex-centric indices when available. A `toString()` of a +Gremlin expression shows a decomposition into individual steps. + +```groovy +gremlin> g.V(hercules).outE('battled').has('time', gt(1)).inV().values('name').toString() +==>[GraphStep([v[24744]],vertex), VertexStep(OUT,[battled],edge), HasStep([time.gt(1)]), EdgeVertexStep(IN), PropertiesStep([name],value)] +``` + +#### More Complex Graph Traversal Examples + +> In the depths of Tartarus lives Pluto. His relationship with Hercules +> was strained by the fact that Hercules battled his pet, Cerberus. +> However, Hercules is his nephew — how should he make Hercules pay for +> his insolence? + +The Gremlin traversals below provide more examples over *The Graph of +the Gods*. The explanation of each traversal is provided in the prior +line as a `//` comment. + +##### Cohabiters of Tartarus +```groovy +gremlin> pluto = g.V().has('name', 'pluto').next() +==>v[2048] +gremlin> // who are pluto's cohabitants? +gremlin> g.V(pluto).out('lives').in('lives').values('name') +==>pluto +==>cerberus +gremlin> // pluto can't be his own cohabitant +gremlin> g.V(pluto).out('lives').in('lives').where(is(neq(pluto))).values('name') +==>cerberus +gremlin> g.V(pluto).as('x').out('lives').in('lives').where(neq('x')).values('name') +==>cerberus +``` + +##### Pluto’s Brothers + +```groovy +gremlin> // where do pluto's brothers live? +gremlin> g.V(pluto).out('brother').out('lives').values('name') +==>sky +==>sea +gremlin> // which brother lives in which place? +gremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god', 'place') +==>[god:v[1024], place:v[512]] +==>[god:v[1280], place:v[768]] +gremlin> // what is the name of the brother and the name of the place? +gremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god', 'place').by('name') +==>[god:jupiter, place:sky] +==>[god:neptune, place:sea] +``` + +Finally, Pluto lives in Tartarus because he shows no concern for death. +His brothers, on the other hand, chose their locations based upon their +love for certain qualities of those locations.! +```groovy +gremlin> g.V(pluto).outE('lives').values('reason') +==>no fear of death +gremlin> g.E().has('reason', textContains('loves')) +==>e[6xs-sg-m51-e8][1024-lives->512] +==>e[70g-zk-m51-lc][1280-lives->768] +gremlin> g.E().has('reason', textContains('loves')).as('source').values('reason').as('reason').select('source').outV().values('name').as('god').select('source').inV().values('name').as('thing').select('god', 'reason', 'thing') +==>[god:neptune, reason:loves waves, thing:sea] +==>[god:jupiter, reason:loves fresh breezes, thing:sky] +``` + +## Architectural Overview +JanusGraph is a graph database engine. JanusGraph itself is focused on +compact graph serialization, rich graph data modeling, and efficient +query execution. In addition, JanusGraph utilizes Hadoop for graph +analytics and batch graph processing. JanusGraph implements robust, +modular interfaces for data persistence, data indexing, and client +access. JanusGraph’s modular architecture allows it to interoperate with +a wide range of storage, index, and client technologies; it also eases +the process of extending JanusGraph to support new ones. + +Between JanusGraph and the disks sits one or more storage and indexing +adapters. JanusGraph comes standard with the following adapters, but +JanusGraph’s modular architecture supports third-party adapters. + +- Data storage: + - [Apache Cassandra](storage-backend/cassandra.md) + - [Apache HBase](storage-backend/hbase.md) + - [Oracle Berkeley DB Java Edition](storage-backend/bdb.md) +- Indices, which speed up and enable more complex queries: + - [Elasticsearch](index-backend/elasticsearch.md) + - [Apache Solr](index-backend/solr.md) + - [Apache Lucene](index-backend/lucene.md) + +Broadly speaking, applications can interact with JanusGraph in two ways: + +- Embed JanusGraph inside the application executing + [Gremlin](http://tinkerpop.apache.org/docs/{{ tinkerpop_version }}/reference#graph-traversal-steps) + queries directly against the graph within the same JVM. Query + execution, JanusGraph’s caches, and transaction handling all happen + in the same JVM as the application while data retrieval from the + storage backend may be local or remote. + +- Interact with a local or remote JanusGraph instance by submitting + Gremlin queries to the server. JanusGraph natively supports the + Gremlin Server component of the [Apache TinkerPop](http://tinkerpop.apache.org/) stack. + +![High-level JanusGraph Architecture and Context](architecture-layer-diagram.svg) diff --git a/docs/indexbackends.adoc b/docs/indexbackends.adoc deleted file mode 100644 index 98ee23fc4e..0000000000 --- a/docs/indexbackends.adoc +++ /dev/null @@ -1,21 +0,0 @@ -[[index-backends]] -= Index Backends - -While JanusGraph's composite graph indexes are natively supported through the primary storage backend, mixed graph indexes require that an indexing backend is configured. Mixed indexes provide support for geo, numeric range, and full-text search. - -The choice of index backend determines which search features are supported, as well as the performance and scalability of the index. JanusGraph currently supports three index backends: <>, <> and <>. - -Use <> or <> when there is an expectation that JanusGraph will be distributed across multiple machines. <> performs better in small scale, single machine applications. It performs better in unit tests, for instance. - -include::searchpredicates.adoc[] - -include::textsearch.adoc[] - -include::directindex.adoc[] - -include::elasticsearch.adoc[] - -include::solr.adoc[] - -include::lucene.adoc[] - diff --git a/docs/inmemorybackend.adoc b/docs/inmemorybackend.adoc deleted file mode 100644 index dd3cc6e54f..0000000000 --- a/docs/inmemorybackend.adoc +++ /dev/null @@ -1,21 +0,0 @@ -[[inmemorystorage]] -== InMemory Storage Backend - -JanusGraph ships with an in-memory storage backend which can be used through the following configuration: - -[source, properties] ----- -storage.backend=inmemory ----- - -Alternatively, an in-memory JanusGraph graph can be opened directly in the Gremlin Console: - -[source, gremlin] -graph = JanusGraphFactory.build().set('storage.backend', 'inmemory').open() - -There are no additional configuration options for the in-memory storage backend. As the name suggests, this backend holds all data in memory. Shutting down the graph or terminating the process that hosts the JanusGraph graph will irrevocably delete all data from the graph. This backend is local to a particular JanusGraph graph instance and cannot be shared across multiple JanusGraph graphs. - -=== Ideal Use Case - -The in-memory storage backend was primarily developed to simplify testing (for those tests that do not require persistence) and graph exploration. The in-memory storage backend is NOT meant for production use, large graphs, or high performance use cases. The in-memory storage backend is not performance or memory optimized. All data is stored in the heap space allocated to the Java virtual machine. - diff --git a/docs/internals.adoc b/docs/internals.adoc deleted file mode 100644 index 411302ab01..0000000000 --- a/docs/internals.adoc +++ /dev/null @@ -1,6 +0,0 @@ -[[internals]] -= JanusGraph Internals - -include::datamodel.adoc[] - -include::building.adoc[] diff --git a/docs/intro.adoc b/docs/intro.adoc deleted file mode 100644 index ba361f86fb..0000000000 --- a/docs/intro.adoc +++ /dev/null @@ -1,339 +0,0 @@ -[[intro]] -= Introduction - -This section gives an overview of JanusGraph's architecture and benefits, -followed by a quick tour of JanusGraph features using a small tutorial data -set. - -[[benefits]] -== The Benefits of JanusGraph - -JanusGraph is designed to support the processing of graphs so large that they require storage and computational capacities beyond what a single machine can provide. Scaling graph data processing for real time traversals and analytical queries is JanusGraph's foundational benefit. This section will discuss the various specific benefits of JanusGraph and its underlying, supported persistence solutions. - -=== General JanusGraph Benefits - -* Support for very large graphs. JanusGraph graphs scale with the number of machines in the cluster. -* Support for very many concurrent transactions and operational graph processing. JanusGraph's transactional capacity scales with the number of machines in the cluster and answers complex traversal queries on huge graphs in milliseconds. -* Support for global graph analytics and batch graph processing through the Hadoop framework. -* Support for geo, numeric range, and full text search for vertices and edges on very large graphs. -* Native support for the popular property graph data model exposed by http://tinkerpop.apache.org/[Apache TinkerPop]. -* Native support for the graph traversal language http://tinkerpop.apache.org/gremlin.html[Gremlin]. -* Easy integration with the http://tinkerpop.apache.org/docs/{tinkerpop_version}/reference/#gremlin-server[Gremlin Server] for programming language agnostic connectivity. -* Numerous graph-level configurations provide knobs for tuning performance. -* Vertex-centric indices provide vertex-level querying to alleviate issues with the infamous http://thinkaurelius.com/2012/10/25/a-solution-to-the-supernode-problem/[super node problem]. -* Provides an optimized disk representation to allow for efficient use of storage and speed of access. -* Open source under the liberal http://en.wikipedia.org/wiki/Apache_License[Apache 2 license]. - -=== Benefits of JanusGraph with Apache Cassandra - -[.tss-floatright.tss-width-125] -image:cassandra-small.svg[link="http://cassandra.apache.org/"] - -* http://en.wikipedia.org/wiki/Continuous_availability[Continuously available] with no single point of failure. -* No read/write bottlenecks to the graph as there is no master/slave architecture. -* http://en.wikipedia.org/wiki/Elastic_computing[Elastic scalability] allows for the introduction and removal of machines. -* Caching layer ensures that continuously accessed data is available in memory. -* Increase the size of the cache by adding more machines to the cluster. -* Integration with http://hadoop.apache.org/[Apache Hadoop]. -* Open source under the liberal Apache 2 license. - - -=== Benefits of JanusGraph with HBase - -[.tss-floatright.tss-width-125] -image:http://hbase.apache.org/images/hbase_logo.png[link="http://hbase.apache.org/"] - -* Tight integration with the http://hadoop.apache.org/[Apache Hadoop] ecosystem. -* Native support for http://en.wikipedia.org/wiki/Strong_consistency[strong consistency]. -* Linear scalability with the addition of more machines. -* http://en.wikipedia.org/wiki/Strict_consistency[Strictly consistent] reads and writes. -* Convenient base classes for backing Hadoop http://en.wikipedia.org/wiki/MapReduce[MapReduce] jobs with HBase tables. -* Support for exporting metrics via http://en.wikipedia.org/wiki/Java_Management_Extensions[JMX]. -* Open source under the liberal Apache 2 license. - -=== JanusGraph and the CAP Theorem - -// The single quotes enable inline text substitutions (required to generate a hyperlink in the output) -[quote, 'http://codahale.com/you-cant-sacrifice-partition-tolerance[Coda Hale]'] -_____________________ - -Despite your best efforts, your system will experience enough faults -that it will have to make a choice between reducing yield (i.e., stop -answering requests) and reducing harvest (i.e., giving answers based -on incomplete data). This decision should be based on business -requirements. -_____________________ - -When using a database, the http://en.wikipedia.org/wiki/CAP_theorem[CAP theorem] should be thoroughly considered (C=Consistency, A=Availability, P=Partitionability). JanusGraph is distributed with 3 supporting backends: http://cassandra.apache.org/[Apache Cassandra], http://hbase.apache.org/[Apache HBase], and http://www.oracle.com/technetwork/database/berkeleydb/overview/index-093405.html[Oracle Berkeley DB Java Edition]. Note that BerkeleyDB JE is a non-distributed database and is typically only used with JanusGraph for testing and exploration purposes. - -HBase gives preference to consistency at the expense of yield, i.e. the probability of completing a request. Cassandra gives preference to availability at the expense of harvest, i.e. the completeness of the answer to the query (data available/complete data). - -[[arch-overview]] -== Architectural Overview - -JanusGraph is a graph database engine. JanusGraph itself is focused on compact -graph serialization, rich graph data modeling, and efficient query -execution. In addition, JanusGraph utilizes Hadoop for graph analytics and batch graph processing. -JanusGraph implements robust, modular interfaces for data -persistence, data indexing, and client access. JanusGraph's modular -architecture allows it to interoperate with a wide range of storage, -index, and client technologies; it also eases the process of extending -JanusGraph to support new ones. - -Between JanusGraph and the disks sits one or more storage and indexing -adapters. JanusGraph comes standard with the following adapters, but -JanusGraph's modular architecture supports third-party adapters. - -* Data storage: -** <> -** <> -** <> -* Indices, which speed up and enable more complex queries: -** <> -** <> -** <> - -Broadly speaking, applications can interact with JanusGraph in two ways: - -* Embed JanusGraph inside the application executing http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference#graph-traversal-steps[Gremlin] queries directly against the graph within the same JVM. Query execution, JanusGraph's caches, and transaction handling all happen in the same JVM as the application while data retrieval from the storage backend may be local or remote. -* Interact with a local or remote JanusGraph instance by submitting Gremlin queries to the server. JanusGraph natively supports the Gremlin Server component of the http://tinkerpop.apache.org/[Apache TinkerPop] stack. - -.High-level JanusGraph Architecture and Context -image::architecture-layer-diagram.svg[] - -[[getting-started]] -== Getting Started - -The examples in this section make extensive use of a toy graph distributed with JanusGraph called _The Graph of the Gods_. This graph is diagrammed below. The abstract data model is known as a http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference#intro[Property Graph Model] and this particular instance describes the relationships between the beings and places of the Roman pantheon. Moreover, special text and symbol modifiers in the diagram (e.g. bold, underline, etc.) denote different schematics/typings in the graph. - - -.Graph of the Gods -image::graph-of-the-gods-2.png[] -[options="header"] -|===== -|visual symbol | meaning -|bold key |a graph indexed key -|bold key with star |a graph indexed key that must have a unique value -|underlined key |a vertex-centric indexed key -|hollow-head edge |a functional/unique edge (no duplicates) -|tail-crossed edge |a unidirectional edge (can only traverse in one direction) -|===== - - -=== Downloading JanusGraph and Running the Gremlin Console - -JanusGraph can be downloaded from the https://github.com/JanusGraph/janusgraph/releases[Releases] section of the project repository. Once retrieved and unpacked, a Gremlin Console can be opened. The Gremlin Console is a http://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop[REPL] (i.e. interactive shell) that is distributed with JanusGraph and only differs from the standard Gremlin Console insofar that JanusGraph is a pre-installed and pre-loaded package. Alternatively, a user may choose to install and activate JanusGraph in an existing Gremlin Console by downloading the JanusGraph package from the central repository. In the example below, `janusgraph.zip` is used, however, be sure to unzip the zip-file that was downloaded. - -[IMPORTANT] -JanusGraph requires Java 8 (Standard Edition). Oracle Java 8 is recommended. JanusGraph's shell scripts expect that the `$JAVA_HOME` environment variable points to the directory where JRE or JDK is installed. - -[source, gremlin] ----- -$ unzip janusgraph-$MAVEN{project.version}-hadoop2.zip -Archive: janusgraph-$MAVEN{project.version}-hadoop2.zip - creating: janusgraph-$MAVEN{project.version}-hadoop2/ -... -$ cd janusgraph-$MAVEN{project.version}-hadoop2 -$ bin/gremlin.sh - - \,,,/ - (o o) ------oOOo-(3)-oOOo----- -09:12:24 INFO org.apache.tinkerpop.gremlin.hadoop.structure.HadoopGraph - HADOOP_GREMLIN_LIBS is set to: /usr/local/janusgraph/lib -plugin activated: tinkerpop.hadoop -plugin activated: janusgraph.imports -gremlin> ----- - -The Gremlin Console interprets commands using http://www.groovy-lang.org/[Apache Groovy]. Groovy is a superset of Java that has various shorthand notations that make interactive programming easier. Likewise Gremlin-Groovy is a superset of Groovy with various shorthand notations that make graph traversals easy. The basic examples below demonstrate handling numbers, strings, and maps. The remainder of the tutorial will discuss graph-specific constructs. - -[source, gremlin] -gremlin> 100-10 -==>90 -gremlin> "JanusGraph:" + " The Rise of Big Graph Data" -==>JanusGraph: The Rise of Big Graph Data -gremlin> [name:'aurelius', vocation:['philosopher', 'emperor']] -==>name=aurelius -==>vocation=[philosopher, emperor] - -[TIP] -Refer to http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference[Apache TinkerPop], http://sql2gremlin.com/[SQL2Gremlin], and http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/recipes/[Gremlin Recipes] for more information about using Gremlin. - -=== Loading the Graph of the Gods Into JanusGraph - -The example below will open a JanusGraph graph instance and load _The Graph of the Gods_ dataset diagrammed above. `JanusGraphFactory` provides a set of static `open` methods, each of which takes a configuration as its argument and returns a graph instance. This tutorial calls one of these `open` methods on a configuration that uses the <> storage backend and the <> index backend, then loads _The Graph of the Gods_ using the helper class `GraphOfTheGodsFactory`. This section skips over the configuration details, but additional information about storage backends, index backends, and their configuration are available in <>, <>, and <>. - -[source, gremlin] -gremlin> graph = JanusGraphFactory.open('conf/janusgraph-berkeleyje-es.properties') -==>standardjanusgraph[berkeleyje:../db/berkeley] -gremlin> GraphOfTheGodsFactory.load(graph) -==>null -gremlin> g = graph.traversal() -==>graphtraversalsource[standardjanusgraph[berkeleyje:../db/berkeley], standard] - -The `JanusGraphFactory.open() and GraphOfTheGodsFactory.load()` methods do the following to the newly constructed graph prior to returning it: - -. Creates a collection of global and vertex-centric indices on the graph. -. Adds all the vertices to the graph along with their properties. -. Adds all the edges to the graph along with their properties. - -Please see the https://github.com/JanusGraph/janusgraph/blob/master/janusgraph-core/src/main/java/org/janusgraph/example/GraphOfTheGodsFactory.java[GraphOfTheGodsFactory source code] for details. - -For those using JanusGraph/Cassandra (or JanusGraph/HBase), be sure to make use of `conf/janusgraph-cql-es.properties` (or `conf/janusgraph-hbase-es.properties`) and `GraphOfTheGodsFactory.load()`. - -[source, gremlin] -gremlin> graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties') -==>standardjanusgraph[cql:[127.0.0.1]] -gremlin> GraphOfTheGodsFactory.load(graph) -==>null -gremlin> g = graph.traversal() -==>graphtraversalsource[standardjanusgraph[cql:[127.0.0.1]], standard] - -You may also use the `conf/janusgraph-cql.properties`, `conf/janusgraph-berkeleyje.properties`, or `conf/janusgraph-hbase.properties` configuration files to open a graph without an indexing backend configured. In such cases, you will need to use the `GraphOfTheGodsFactory.loadWithoutMixedIndex()` method to load the _Graph of the Gods_ so that it doesn't attempt to make use of an indexing backend. - -[source, gremlin] -gremlin> graph = JanusGraphFactory.open('conf/janusgraph-cql.properties') -==>standardjanusgraph[cql:[127.0.0.1]] -gremlin> GraphOfTheGodsFactory.loadWithoutMixedIndex(graph, true) -==>null -gremlin> g = graph.traversal() -==>graphtraversalsource[standardjanusgraph[cql:[127.0.0.1]], standard] - -=== Global Graph Indices - -The typical pattern for accessing data in a graph database is to first locate the entry point into the graph using a graph index. That entry point is an element (or set of elements) -- i.e. a vertex or edge. From the entry elements, a Gremlin path description describes how to traverse to other elements in the graph via the explicit graph structure. - -Given that there is a unique index on `name` property, the Saturn vertex can be retrieved. The property map (i.e. the key/value pairs of Saturn) can then be examined. As demonstrated, the Saturn vertex has a `name` of "saturn, " an `age` of 10000, and a `type` of "titan." The grandchild of Saturn can be retrieved with a traversal that expresses: "Who is Saturn's grandchild?" (the inverse of "father" is "child"). The result is Hercules. - -[source, gremlin] -gremlin> saturn = g.V().has('name', 'saturn').next() -==>v[256] -gremlin> g.V(saturn).valueMap() -==>[name:[saturn], age:[10000]] -gremlin> g.V(saturn).in('father').in('father').values('name') -==>hercules - -The property `place` is also in a graph index. The property `place` is an edge property. Therefore, JanusGraph can index edges in a graph index. It is possible to query _The Graph of the Gods_ for all events that have happened within 50 kilometers of http://en.wikipedia.org/wiki/Athens[Athens] (latitude:37.97 and long:23.72). Then, given that information, which vertices were involved in those events. - -[source, gremlin] -gremlin> g.E().has('place', geoWithin(Geoshape.circle(37.97, 23.72, 50))) -==>e[a9x-co8-9hx-39s][16424-battled->4240] -==>e[9vp-co8-9hx-9ns][16424-battled->12520] -gremlin> g.E().has('place', geoWithin(Geoshape.circle(37.97, 23.72, 50))).as('source').inV().as('god2').select('source').outV().as('god1').select('god1', 'god2').by('name') -==>[god1:hercules, god2:hydra] -==>[god1:hercules, god2:nemean] - -Graph indices are one type of index structure in JanusGraph. Graph indices are automatically chosen by JanusGraph to answer which ask for all vertices (`g.V`) or all edges (`g.E`) that satisfy one or multiple constraints (e.g. `has` or `interval`). The second aspect of indexing in JanusGraph is known as vertex-centric indices. Vertex-centric indices are utilized to speed up traversals inside the graph. Vertex-centric indices are described later. - -==== Graph Traversal Examples - -[quote] -http://en.wikipedia.org/wiki/Hercules[Hercules], son of Jupiter and http://en.wikipedia.org/wiki/Alcmene[Alcmene], bore super human strength. Hercules was a http://en.wikipedia.org/wiki/Demigod[Demigod] because his father was a god and his mother was a human. http://en.wikipedia.org/wiki/Juno_(mythology)[Juno], wife of Jupiter, was furious with Jupiter's infidelity. In revenge, she blinded Hercules with temporary insanity and caused him to kill his wife and children. To atone for the slaying, Hercules was ordered by the http://en.wikipedia.org/wiki/Oracle_at_Delphi[Oracle of Delphi] to serve http://en.wikipedia.org/wiki/Eurystheus[Eurystheus]. Eurystheus appointed Hercules to 12 labors. - -//.. .. figure:: http://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Mosaico_Trabajos_H%C3%A9rcules_%28M.A.N._Madrid%29_01.jpg/301px-Mosaico_Trabajos_H%C3%A9rcules_%28M.A.N._Madrid%29_01.jpg -//.. :align: right -//.. -//.. Nemean -//.. -//.. .. figure:: http://upload.wikimedia.org/wikipedia/commons/thumb/2/23/Hercules_slaying_the_Hydra.jpg/320px-Hercules_slaying_the_Hydra.jpg -//.. :align: right -//.. -//.. Hydra -//.. -//.. .. figure:: http://upload.wikimedia.org/wikipedia/commons/thumb/2/25/Cerberus-Blake.jpeg/320px-Cerberus-Blake.jpeg -//.. :align: right -//.. -//.. Cerberus - -In the previous section, it was demonstrated that Saturn's grandchild was Hercules. This can be expressed using a `loop`. In essence, Hercules is the vertex that is 2-steps away from Saturn along the `in('father')` path. - -[source, gremlin] -gremlin> hercules = g.V(saturn).repeat(__.in('father')).times(2).next() -==>v[1536] - -Hercules is a demigod. To prove that Hercules is half human and half god, his parent's origins must be examined. It is possible to traverse from the Hercules vertex to his mother and father. Finally, it is possible to determine the `type` of each of them -- yielding "god" and "human." - -[source, gremlin] -gremlin> g.V(hercules).out('father', 'mother') -==>v[1024] -==>v[1792] -gremlin> g.V(hercules).out('father', 'mother').values('name') -==>jupiter -==>alcmene -gremlin> g.V(hercules).out('father', 'mother').label() -==>god -==>human -gremlin> hercules.label() -==>demigod - -The examples thus far have been with respect to the genetic lines of the various actors in the Roman pantheon. The http://tinkerpop.apache.org/docs/$MAVEN{tinkerpop.version}/reference#intro[Property Graph Model] is expressive enough to represent multiple types of things and relationships. In this way, _The Graph of the Gods_ also identifies Hercules' various heroic exploits --- his famous 12 labors. In the previous section, it was discovered that Hercules was involved in two battles near Athens. It is possible to explore these events by traversing `battled` edges out of the Hercules vertex. - -[source, gremlin] -gremlin> g.V(hercules).out('battled') -==>v[2304] -==>v[2560] -==>v[2816] -gremlin> g.V(hercules).out('battled').valueMap() -==>[name:[nemean]] -==>[name:[hydra]] -==>[name:[cerberus]] -gremlin> g.V(hercules).outE('battled').has('time', gt(1)).inV().values('name') -==>cerberus -==>hydra - -The edge property `time` on `battled` edges is indexed by the vertex-centric indices of a vertex. Retrieving `battled` edges incident to Hercules according to a constraint/filter on `time` is faster than doing a linear scan of all edges and filtering (typically `O(log n)`, where `n` is the number incident edges). JanusGraph is intelligent enough to use vertex-centric indices when available. A `toString()` of a Gremlin expression shows a decomposition into individual steps. - -[source, gremlin] -gremlin> g.V(hercules).outE('battled').has('time', gt(1)).inV().values('name').toString() -==>[GraphStep([v[24744]],vertex), VertexStep(OUT,[battled],edge), HasStep([time.gt(1)]), EdgeVertexStep(IN), PropertiesStep([name],value)] - -==== More Complex Graph Traversal Examples - -[quote] -In the depths of Tartarus lives Pluto. His relationship with Hercules was strained by the fact that Hercules battled his pet, Cerberus. However, Hercules is his nephew -- how should he make Hercules pay for his insolence? - -The Gremlin traversals below provide more examples over _The Graph of the Gods_. The explanation of each traversal is provided in the prior line as a `//` comment. - -===== Cohabiters of Tartarus - -[source, gremlin] -gremlin> pluto = g.V().has('name', 'pluto').next() -==>v[2048] -gremlin> // who are pluto's cohabitants? -gremlin> g.V(pluto).out('lives').in('lives').values('name') -==>pluto -==>cerberus -gremlin> // pluto can't be his own cohabitant -gremlin> g.V(pluto).out('lives').in('lives').where(is(neq(pluto))).values('name') -==>cerberus -gremlin> g.V(pluto).as('x').out('lives').in('lives').where(neq('x')).values('name') -==>cerberus - -===== Pluto's Brothers - -[source, gremlin] -gremlin> // where do pluto's brothers live? -gremlin> g.V(pluto).out('brother').out('lives').values('name') -==>sky -==>sea -gremlin> // which brother lives in which place? -gremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god', 'place') -==>[god:v[1024], place:v[512]] -==>[god:v[1280], place:v[768]] -gremlin> // what is the name of the brother and the name of the place? -gremlin> g.V(pluto).out('brother').as('god').out('lives').as('place').select('god', 'place').by('name') -==>[god:jupiter, place:sky] -==>[god:neptune, place:sea] - -Finally, Pluto lives in Tartarus because he shows no concern for death. His brothers, on the other hand, chose their locations based upon their love for certain qualities of those locations. - -[source, gremlin] -gremlin> g.V(pluto).outE('lives').values('reason') -==>no fear of death -gremlin> g.E().has('reason', textContains('loves')) -==>e[6xs-sg-m51-e8][1024-lives->512] -==>e[70g-zk-m51-lc][1280-lives->768] -gremlin> g.E().has('reason', textContains('loves')).as('source').values('reason').as('reason').select('source').outV().values('name').as('god').select('source').inV().values('name').as('thing').select('god', 'reason', 'thing') -==>[god:neptune, reason:loves waves, thing:sea] -==>[god:jupiter, reason:loves fresh breezes, thing:sky] - diff --git a/docs/janusgraph-logomark.svg b/docs/janusgraph-logomark.svg new file mode 100644 index 0000000000..53d15a16fc --- /dev/null +++ b/docs/janusgraph-logomark.svg @@ -0,0 +1 @@ +JanusGraph logomark black rev RGB \ No newline at end of file diff --git a/docs/janusgraph.png b/docs/janusgraph.png new file mode 100644 index 0000000000000000000000000000000000000000..9a18e1fc6e08d795b0dc2480c6d9d7cc6e02d483 GIT binary patch literal 14442 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 literal 0 HcmV?d00001 diff --git a/docs/listings/janusgraph_cfg.adoc b/docs/listings/janusgraph_cfg.adoc deleted file mode 100644 index 6c0e1cd719..0000000000 --- a/docs/listings/janusgraph_cfg.adoc +++ /dev/null @@ -1 +0,0 @@ -// This file is generated automatically, do not edit diff --git a/docs/lucene.adoc b/docs/lucene.adoc deleted file mode 100644 index e536ab7610..0000000000 --- a/docs/lucene.adoc +++ /dev/null @@ -1,40 +0,0 @@ -[[lucene]] -== Apache Lucene - -[quote, 'http://lucene.apache.org/[Apache Lucene Homepage]'] -Apache Lucene is a high-performance, full-featured text search engine library written entirely in Java. It is a technology suitable for nearly any application that requires full-text search, especially cross-platform. Apache Lucene is an open source project available for free download. - -JanusGraph supports http://lucene.apache.org/[Apache Lucene] as a single-machine, embedded index backend. Lucene has a slightly extended feature set and performs better in small-scale applications compared to <>, but is limited to single-machine deployments. - -=== Lucene Embedded Configuration - -For single machine deployments, Lucene runs embedded with JanusGraph. JanusGraph starts and interfaces with Lucene internally. - -To run Lucene embedded, add the following configuration options to the graph configuration file where `/data/searchindex` specifies the directory where Lucene should store the index data: - -[source, properties] -index.search.backend=lucene -index.search.directory=/data/searchindex - -In the above configuration, the index backend is named `search`. Replace `search` by a different name to change the name of the index. - -=== Feature Support - -* *Full-Text*: Supports all `Text` predicates to search for text properties that matches a given word, prefix or regular expression. -* *Geo*: Supports `Geo` predicates to search for geo properties that are intersecting, within, or contained in a given query geometry. Supports points, lines and polygons for indexing. Supports circles and boxes for querying point properties and all shapes for querying non-point properties. -* *Numeric Range*: Supports all numeric comparisons in `Compare`. -* *Temporal*: Nanosecond granularity temporal indexing. - -=== Configuration Options - -Refer to <> for a complete listing of all Lucene specific configuration options in addition to the general JanusGraph configuration options. - -Note, that each of the index backend options needs to be prefixed with `index.[INDEX-NAME].` where `[INDEX-NAME]` stands for the name of the index backend. For instance, if the index backend is named _search_ then these configuration options need to be prefixed with `index.search.`. -To configure an index backend named _search_ to use Lucene as the index system, set the following configuration option: - -[source, properties] -index.search.backend=lucene - -=== Further Reading - -* Please refer to the http://lucene.apache.org/[Apache Lucene homepage] and available documentation for more information on Lucene. diff --git a/docs/migrating.adoc b/docs/migrating.adoc deleted file mode 100644 index d32b266ac9..0000000000 --- a/docs/migrating.adoc +++ /dev/null @@ -1,35 +0,0 @@ -[[migrating-titan]] -== Migrating from Titan - -This page describes some of the Configuration options that JanusGraph provides to allow migration of data from a data store which had previously been created by Titan. - -=== Configuration - -When connecting to an existing Titan data store the `graph.titan-version` property should already be set in the global configuration to Titan version `1.0.0`. The ID store name in JanusGraph is configurable via the `ids.store-name` property whereas in Titan it was a constant. If the `graph.titan-version` has been set in the existing global configuration, then you do **not** need to explicitly set the ID store as it will default to `titan_ids`. - - -=== Cassandra - -The default keyspace used by Titan was `titan` and in order to reuse that existing keyspace the `storage.cassandra.keyspace` property needs to be set accordingly. - -[source, properties] ----- -storage.cassandra.keyspace=titan ----- - -These configuration options allow JanusGraph to read data from a Cassandra database which had previously been created by Titan. However, once JanusGraph writes back to that database it will register additional serializers which mean that it will no longer be compatible with Titan. Users are therefore encouraged to backup the data in Casssandra before attempting to use it with the JanusGraph release. - -=== HBase - -The name of the table used by Titan was `titan` and in order to reuse that existing table the `storage.hbase.table` property needs to be set accordingly. - -[source, properties] ----- -storage.hbase.table=titan ----- - -These configuration options allow JanusGraph to read data from an HBase database which had previously been created by Titan. However, once JanusGraph writes back to that database it will register additional serializers which mean that it will no longer be compatible with Titan. Users are therefore encouraged to backup the data in HBase before attempting to use it with the JanusGraph release. - -=== BerkeleyDB - -The BerkeleyDB version has been updated, and it contains changes to the file format stored on disk. This file format change is forward compatible with previous versions of BerkeleyDB, so existing graph data stored with Titan can be read in. However, once the data has been read in with the newer version of BerkeleyDB, those files can no longer be read by the older version. Users are encouraged to backup the BerkeleyDB storage directory before attempting to use it with the JanusGraph release. diff --git a/docs/monitoring.adoc b/docs/monitoring.adoc deleted file mode 100644 index 5e8d22c35f..0000000000 --- a/docs/monitoring.adoc +++ /dev/null @@ -1,217 +0,0 @@ -[[monitoring]] -== Monitoring JanusGraph - -=== Metrics in JanusGraph - -JanusGraph supports http://dropwizard.io/[Metrics]. JanusGraph can measure the following: - -* The number of transactions begun, committed, and rolled back -* The number of attempts and failures of each storage backend operation type -* The response time distribution of each storage backend operation type - -==== Configuring Metrics Collection - -To enable Metrics collection, set the following in JanusGraph's properties file: - -[source, properties] ----- -# Required to enable Metrics in JanusGraph -metrics.enabled = true ----- - -This setting makes JanusGraph record measurements at runtime using Metrics classes like Timer, Counter, Histogram, etc. To access these measurements, one or more Metrics reporters must be configured as described in the section <>. - -===== Customizing the Default Metric Names - -JanusGraph prefixes all metric names with "org.janusgraph" by default. This prefix can be set through the `metrics.prefix` configuration property. For example, to shorten the default "org.janusgraph" prefix to just "janusgraph": - -[source, properties] ----- -# Optional -metrics.prefix = janusgraph ----- - -Transaction-Specific Metrics Names -+++++++++++++++++++++++++++++++++ - -Each JanusGraph transaction may optionally specify its own Metrics name prefix, overriding both the default Metrics name prefix and the `metrics.prefix` configuration property. For example, the prefix could be changed to the name of the frontend application that opened the JanusGraph transaction. Note that Metrics maintains a ConcurrentHashMap of metric names and their associated objects in memory, so it's probably a good idea to keep the number of distinct metric prefixes small. - -To do this, call `TransactionBuilder.setMetricsPrefix(String)`: - -[source, java] -JanusGraph graph = ...; -TransactionBuilder tbuilder = graph.buildTransaction(); -JanusGraphTransaction tx = tbuilder.groupName("foobar").start(); - -===== Separating Metrics by Backend Store - -JanusGraph combines the Metrics for its various internal storage backend handles by default. All Metrics for storage backend interactions follow the pattern ".stores.", regardless of whether they come from the ID store, edge store, etc. When `metrics.merge-basic-metrics = false` is set in JanusGraph's properties file, the "stores" string in metric names is replaced by "idStore", "edgeStore", "vertexIndexStore", or "edgeIndexStore". - -[[metrics-reporters]] -=== Configuring Metrics Reporting - -JanusGraph supports the following Metrics reporters: - -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -Each reporter type is independent of and can coexist with the others. For example, it's possible to configure Ganglia, JMX, and Slf4j Metrics reporters to operate simultaneously. Just set all their respective configuration keys in janusgraph.properties (and enable metrics as directed above). - -[[metrics-console]] -==== Console Reporter - -.Metrics Console Reporter Configuration Options -[cols="2,1,5,1", options="header"] -|========================== -|Config Key |Required? |Value |Default -|metrics.console.interval |yes |Milliseconds to wait between dumping metrics to the console |null -|========================== - -Example janusgraph.properties snippet that prints metrics to the console once a minute: - -[source, properties] -metrics.enabled = true -# Required; specify logging interval in milliseconds -metrics.console.interval = 60000 - -[[metrics-csv]] -==== CSV File Reporter - -.Metrics CSV Reporter Configuration Options -[cols="2,1,5,1", options="header"] -|========================== -|Config Key |Required? |Value |Default -|metrics.csv.interval |yes |Milliseconds to wait between writing CSV lines |null -|metrics.csv.directory |yes |Directory in which CSV files are written (will be created if it does not exist) |null -|========================== - -Example janusgraph.properties snippet that writes CSV files once a minute to the directory `./foo/bar/` (relative to the process's working directory): - -[source, properties] -metrics.enabled = true -# Required; specify logging interval in milliseconds -metrics.csv.interval = 60000 -metrics.csv.directory = foo/bar - -[[metrics-ganglia]] -==== Ganglia Reporter - -NOTE: Configuration of link:http://ganglia.sourceforge.net/[Ganglia] requires an additional library that is not packaged with JanusGraph due to its LGPL licensing that conflicts with the JanusGraph's Apache 2.0 License. To run with Ganglia monitoring, download the `org.acplt:oncrpc` jar from link:http://repo1.maven.org/maven2/org/acplt/oncrpc/1.0.7/[here] and copy it to the JanusGraph `/lib` directory before starting the server. - -.Metrics Ganglia Reporter Configuration Options -[cols="2,1,5,1", options="header"] -|========================== -|Config Key |Required? |Value |Default -|metrics.ganglia.hostname |yes |Unicast host or multicast group to which our Metrics are sent | null -|metrics.ganglia.interval |yes |Milliseconds to wait between sending datagrams | null -|metrics.ganglia.port |no |UDP port to which we send Metrics datagrams | 8649 -|metrics.ganglia.addressing-mode |no |Must be "unicast" or "multicast" | unicast -|metrics.ganglia.ttl |no |Multicast datagram TTL; ignore for unicast | 1 -|metrics.ganglia.protocol-31 |no |Boolean; true to use Ganglia protocol 3.1, false to use 3.0 | true -|metrics.ganglia.uuid |no |https://github.com/ganglia/monitor-core/wiki/UUIDSources[Host UUID to report instead of IP:hostname] | null -|metrics.ganglia.spoof |no |https://github.com/ganglia/monitor-core/wiki/Gmetric-Spoofing[Override IP:hostname reported to Ganglia] | null -|========================== - -Example janusgraph.properties snippet that sends unicast UDP datagrams to localhost on the default port once every 30 seconds: - -[source, properties] -metrics.enabled = true -# Required; IP or hostname string -metrics.ganglia.hostname = 127.0.0.1 -# Required; specify logging interval in milliseconds -metrics.ganglia.interval = 30000 - -Example janusgraph.properties snippet that sends unicast UDP datagrams to a non-default destination port and which also spoofs the IP and hostname reported to Ganglia: - -[source, properties] -metrics.enabled = true -# Required; IP or hostname string -metrics.ganglia.hostname = 1.2.3.4 -# Required; specify logging interval in milliseconds -metrics.ganglia.interval = 60000 -# Optional -metrics.ganglia.port = 6789 -metrics.ganglia.spoof = 10.0.0.1:zombo.com - -[[metrics-graphite]] -==== Graphite Reporter - -.Metrics Graphite Reporter Configuration Options -[cols="2,1,5,1", options="header"] -|========================== -|Config Key |Required? |Value |Default -|metrics.graphite.hostname |yes |IP address or hostname to which https://graphite.readthedocs.org/en/latest/feeding-carbon.html#the-plaintext-protocol[Graphite plaintext protocol] data are sent |null -|metrics.graphite.interval |yes |Milliseconds to wait between pushing data to Graphite |null -|metrics.graphite.port |no |Port to which Graphite plaintext protocol reports are sent |2003 -|metrics.graphite.prefix |no |Arbitrary string prepended to all metric names sent to Graphite |null -|========================== - -Example janusgraph.properties snippet that sends metrics to a Graphite server on 192.168.0.1 every minute: - -[source, properties] -metrics.enabled = true -# Required; IP or hostname string -metrics.graphite.hostname = 192.168.0.1 -# Required; specify logging interval in milliseconds -metrics.graphite.interval = 60000 - -[[metrics-jmx]] -==== JMX Reporter - -.Metrics JMX Reporter Configuration Options -[cols="2,1,5,1", options="header"] -|========================== -|Config Key |Required? |Value |Default -|metrics.jmx.enabled |yes |Boolean |false -|metrics.jmx.domain |no |Metrics will appear in this JMX domain |Metrics's own default -|metrics.jmx.agentid |no |Metrics will be reported with this JMX agent ID |Metrics's own default -|========================== - -Example janusgraph.properties snippet: - -[source, properties] -metrics.enabled = true -# Required -metrics.jmx.enabled = true -# Optional; if omitted, then Metrics uses its default values -metrics.jmx.domain = foo -metrics.jmx.agentid = baz - -[[metrics-slf4j]] -==== Slf4j Reporter - -.Metrics Slf4j Reporter Configuration Options -[cols="2,1,5,1", options="header"] -|========================== -|Config Key |Required? |Value |Default -|metrics.slf4j.interval |yes |Milliseconds to wait between dumping metrics to the logger |null -|metrics.slf4j.logger |no |Slf4j logger name to use |"metrics" -|========================== - -Example janusgraph.properties snippet that logs metrics once a minute to the logger named `foo`: - -[source, properties] -metrics.enabled = true -# Required; specify logging interval in milliseconds -metrics.slf4j.interval = 60000 -# Optional; uses Metrics default when unset -metrics.slf4j.logger = foo - -[[metrics-custom]] -==== User-Provided/Custom Reporter - -In case the Metrics reporter configuration options listed above are insufficient, JanusGraph provides a utility method to access the single `MetricRegistry` instance which holds all of its measurements. - -[source, java] ----- -com.codahale.metrics.MetricRegistry janusgraphRegistry = - org.janusgraph.util.stats.MetricManager.INSTANCE.getRegistry(); ----- - -Code that accesses `janusgraphRegistry` this way can then attach non-standard reporter types or standard reporter types with exotic configurations to `janusgraphRegistry`. This approach is also useful if the surrounding application already has a framework for Metrics reporter configuration, or if the application needs multiple differently-configured instances of one of JanusGraph's supported reporter types. For instance, one could use this approach to setup multiple unicast Graphite reporters whereas JanusGraph's properties configuration is limited to just one Graphite reporter. - diff --git a/docs/partitioning.adoc b/docs/partitioning.adoc deleted file mode 100644 index 1d2b3c965c..0000000000 --- a/docs/partitioning.adoc +++ /dev/null @@ -1,53 +0,0 @@ -[[graph-partitioning]] -== Graph Partitioning - -When JanusGraph is deployed on a cluster of multiple storage backend instances, the graph is partitioned across those machines. Since JanusGraph stores the graph in an adjacency list representation the assignment of vertices to machines determines the partitioning. By default, JanusGraph uses a random partitioning strategy that randomly assigns vertices to machines. Random partitioning is very efficient, requires no configuration, and results in balanced partitions. However, random partitioning results in less efficient query processing as the JanusGraph cluster grows to accommodate more graph data because of the increasing cross-instance communication required to retrieve the query's result set. Explicit graph partitioning can ensure that strongly connected and frequently traversed subgraphs are stored on the same instance thereby reducing the communication overhead significantly. - -To enable explicit graph partitioning in JanusGraph, the following configuration options must be set when the JanusGraph cluster is initialized. - -[source, properties] -cluster.partition = true -cluster.max-partitions = 32 -ids.flush = false - -The configuration option `max-partitions` controls how many virtual partitions JanusGraph creates. This number should be roughly twice the number of storage backend instances. If the cluster of storage backend instances is expected to grow, estimate the size of the cluster in the foreseeable future and take this number as the baseline. Setting this number too large will unnecessarily fragment the cluster which can lead to poor performance. - -Because explicit graph partitioning controls the assignment of vertices to storage instances it cannot be enabled once a JanusGraph cluster is initialized. Likewise, the number of virtual partitions cannot be changed without reloading the graph. - -Explicit graph partitioning can only enabled against storage backends that support ordered key storage: - -* *HBase*: Always supports explicit graph partitioning -* *Cassandra*: Must be configured to use *ByteOrderedPartitioner* in order to support explicit graph partitioning - -There are two aspects to graph partitioning which can be individually controlled: edge cuts and vertex cuts. - -=== Edge Cut - -In assigning vertices to partitions one strives to optimize the assignment such that frequently co-traversed vertices are hosted on the same machine. Assume vertex A is assigned to machine 1 and vertex B is assigned to machine 2. An edge between the vertices is called a *cut edge* because its end points are hosted on separate machines. Traversing this edge as part of a graph query requires communication between the machines which slows down query processing. Hence, it is desirable to reduce the edge cut for frequently traversed edges. That, in turn, requires placing the adjacent vertices of frequently traversed edges in the same partition. - -Vertices are placed in a partition by way of the assigned vertex id. A partition is essentially a sequential range of vertex ids. To place a vertex in a particular partition, JanusGraph chooses an id from the partition's range of vertex ids. JanusGraph controls the vertex-to-partition assignment through the configured placement strategy. By default, vertices created in the same transaction are assigned to the same partition. This strategy is easy to reason about and works well in situations where frequently co-traversed vertices are created in the same transaction - either by optimizing the loading strategy to that effect or because vertices are naturally added to the graph that way. However, the strategy is limited, leads to imbalanced partitions when data is loaded in large transactions and not the optimal strategy for many use cases. The user can provide a use case specific vertex placement strategy by implementing the `IDPlacementStrategy` interface and registering it in the configuration through the `ids.placement` option. - -When implementing `IDPlacementStrategy`, note that partitions are identified by an integer id in the range from 0 to the number of configured virtual partitions minus 1. For our example configuration, there are partitions 0, 1, 2, 3, ..31. Partition ids are not the same as vertex ids. Edge cuts are more meaningful when the JanusGraph servers are on the same hosts as the storage backend. If you have to make a network call to a different host on each hop of a traversal, the benefit of edge cuts and custom placement strategies can be largely nullified. - -=== Vertex Cut - -While edge cut optimization aims to reduce the cross communication and thereby improve query execution, vertex cuts address the hotspot issue caused by vertices with a large number of incident edges. While <> effectively address query performance for large degree vertices, vertex cuts are needed to address the hot spot issue on very large graphs. - -Cutting a vertex means storing a subset of that vertex's adjacency list on each partition in the graph. In other words, the vertex and its adjacency list is partitioned thereby effectively distributing the load on that single vertex across all of the instances in the cluster and removing the hot spot. - -JanusGraph cuts vertices by label. A vertex label can be defined as _partitioned_ which means that all vertices of that label will be partitioned across the cluster in the manner described above. - -[source, gremlin] -mgmt = graph.openManagement() -mgmt.makeVertexLabel('user').make() -mgmt.makeVertexLabel('product').partition().make() -mgmt.commit() - -In the example above, `product` is defined as a partitioned vertex label whereas `user` is a normal label. This configuration is beneficial for situations where there are thousands of products but millions of users and one records transactions between users and products. In that case, the product vertices will have a very high degree and the popular products turns into hot spots if they are not partitioned. - -=== Graph Partitioning FAQ - -==== Random vs. Explicit Partitioning - -When the graph is small or accommodated by a few storage instances, it is best to use random partitioning for its simplicity. As a rule of thumb, one should strongly consider enabling explicit graph partitioning and configure a suitable partitioning heuristic when the graph grows into the 10s of billions of edges. - diff --git a/docs/recovery.adoc b/docs/recovery.adoc deleted file mode 100644 index 07b560649f..0000000000 --- a/docs/recovery.adoc +++ /dev/null @@ -1,51 +0,0 @@ -[[failure-recovery]] -== Failure & Recovery - -JanusGraph is a highly available and robust graph database. In large scale JanusGraph deployments failure is inevitable. This page describes some failure situations and how JanusGraph can handle them. - -=== Transaction Failure - -Transactions can fail for a number of reasons. If the transaction fails before the commit the changes will be discarded and the application can retry the transaction in coherence with the business logic. Likewise, locking or other consistency failures will cause an exception prior to persistence and hence can be retried. -The persistence stage of a transaction is when JanusGraph starts persisting data to the various backend systems. - -JanusGraph first persists all graph mutations to the storage backend. This persistence is executed as one batch mutation to ensure that the mutation is committed atomically for those backends supporting atomicity. If the batch mutation fails due to an exception in the storage backend, the entire transaction is failed. - -If the primary persistence into the storage backend succeeds but secondary persistence into the indexing backends or the logging system fail, the transaction is still considered to be successful because the storage backend is the authoritative source of the graph. - -However, this can create inconsistencies with the indexes and logs. To automatically repair such inconsistencies, JanusGraph can maintain a transaction write-ahead log which is enabled through the configuration. - -[source, properties] -tx.log-tx = true -tx.max-commit-time = 10000 - -The max-commit-time property is used to determine when a transaction has failed. If the persistence stage of the transaction takes longer than this time, JanusGraph will attempt to recover it if necessary. Hence, this time out should be configured as a generous upper bound on the maximum duration of persistence. Note, that this does not include the time spent before commit. - -In addition, a separate process must be setup that reads the log to identify partially failed transaction and repair any inconsistencies caused. It is suggested to run the transaction repair process on a separate machine connected to the cluster to isolate failures. Configure a separately controlled process to run the following where the start time specifies the time since epoch where the recovery process should start reading from the write-ahead log. - -[source, gremlin] -recovery = JanusGraphFactory.startTransactionRecovery(graph, startTime, TimeUnit.MILLISECONDS); - -Enabling the transaction write-ahead log causes an additional write operation for mutating transactions which increases the latency. Also note, that additional space is required to store the log. The transaction write-ahead log has a configurable time-to-live of 2 days which means that log entries expire after that time to keep the storage overhead small. Refer to <> for a complete list of all log related configuration options to fine tune logging behavior. - -=== JanusGraph Instance Failure - -JanusGraph is robust against individual instance failure in that other instances of the JanusGraph cluster are not impacted by such failure and can continue processing transactions without loss of performance while the failed instance is restarted. - -However, some schema related operations - such as installing indexes - require the coordination of all JanusGraph instances. For this reason, JanusGraph maintains a record of all running instances. If an instance fails, i.e. is not properly shut down, JanusGraph considers it to be active and expects its participation in cluster-wide operations which subsequently fail because this instances did not participate in or did not acknowledge the operation. - -In this case, the user must manually remove the failed instance record from the cluster and then retry the operation. -To remove the failed instance, open a management transaction against any of the running JanusGraph instances, inspect the list of running instances to identify the failed one, and finally remove it. - -[source, gremlin] -mgmt = graph.openManagement() -mgmt.getOpenInstances() //all open instances -==>7f0001016161-dunwich1(current) -==>7f0001016161-atlantis1 -mgmt.forceCloseInstance('7f0001016161-atlantis1') //remove an instance -mgmt.commit() - -The unique identifier of the current JanusGraph instance is marked with the suffix `(current)` so that it can be easily identified. This instance cannot be closed via the `forceCloseInstance` method and instead should be closed via `g.close()` - - - -It must be ensured that the manually removed instance is indeed no longer active. Removing an active JanusGraph instance from a cluster can cause data inconsistencies. Hence, use this method with great care in particular when JanusGraph is operated in an environment where instances are automatically restarted. diff --git a/docs/reindex.adoc b/docs/reindex.adoc deleted file mode 100644 index 7fee1b0920..0000000000 --- a/docs/reindex.adoc +++ /dev/null @@ -1,380 +0,0 @@ -[[index-admin]] -== Index Management - -[[reindex]] -=== Reindexing - -<> and <> describe how to build graph-global and vertex-centric indexes to improve query performance. These indexes are immediately available if the indexed keys or labels have been newly defined in the same management transaction. In this case, there is no need to reindex the graph and this section can be skipped. If the indexed keys and labels already existed prior to index construction it is necessary to reindex the entire graph in order to ensure that the index contains previously added elements. This section describes the reindexing process. - -[WARNING] -Reindexing is a manual process comprised of multiple steps. These steps must be carefully followed in the right order to avoid index inconsistencies. - -==== Overview - -JanusGraph can begin writing incremental index updates right after an index is defined. However, before the index is complete and usable, JanusGraph must also take a one-time read pass over all existing graph elements associated with the newly indexed schema type(s). Once this reindexing job has completed, the index is fully populated and ready to be used. The index must then be enabled to be used during query processing. - -==== Prior to Reindex - -The starting point of the reindexing process is the construction of an index. Refer to <> for a complete discussion of global graph and vertex-centric indexes. Note, that a global graph index is uniquely identified by its name. A vertex-centric index is uniquely identified by the combination of its name and the edge label or property key on which the index is defined - the name of the latter is referred to as the *index type* in this section and only applies to vertex-centric indexes. - -After building a new index against existing schema elements it is recommended to wait a few minutes for the index to be announced to the cluster. Note the index name (and the index type in case of a vertex-centric index) since this information is needed when reindexing. - -==== Preparing to Reindex - -There is a choice between two execution frameworks for reindex jobs: - -* MapReduce -* JanusGraphManagement - -Reindex on MapReduce supports large, horizontally-distributed databases. Reindex on JanusGraphManagement spawns a single-machine OLAP job. This is intended for convenience and speed on those databases small enough to be handled by one machine. - -Reindexing requires: - -* The index name (a string -- the user provides this to JanusGraph when building a new index) -* The index type (a string -- the name of the edge label or property key on which the vertex-centric index is built). This applies only to vertex-centric indexes - leave blank for global graph indexes. - -==== Executing a Reindex Job on MapReduce - -The recommended way to generate and run a reindex job on MapReduce is through the `MapReduceIndexManagement` class. Here is a rough outline of the steps to run a reindex job using this class: - -* Open a `JanusGraph` instance -* Pass the graph instance into `MapReduceIndexManagement`'s constructor -* Call `updateIndex(, SchemaAction.REINDEX)` on the `MapReduceIndexManagement` instance -* If the index has not yet been enabled, enable it through `JanusGraphManagement` - -This class implements an `updateIndex` method that supports only the `REINDEX` and `REMOVE_INDEX` actions for its `SchemaAction` parameter. The class starts a Hadoop MapReduce job using the Hadoop configuration and jars on the classpath. Both Hadoop 1 and 2 are supported. This class gets metadata about the index and storage backend (e.g. the Cassandra partitioner) from the `JanusGraph` instance given to its constructor. - -[source,gremlin] -graph = JanusGraphFactory.open(...) -mgmt = graph.openManagement() -mr = new MapReduceIndexManagement(graph) -mr.updateIndex(mgmt.getRelationIndex(mgmt.getRelationType("battled"), "battlesByTime"), SchemaAction.REINDEX).get() -mgmt.commit() - -===== Reindex Example on MapReduce - -The following Gremlin snippet outlines all steps of the MapReduce reindex process in one self-contained example using minimal dummy data against the Cassandra storage backend. - -[source,gremlin] ----- -// Open a graph -graph = JanusGraphFactory.open("conf/janusgraph-cql-es.properties") -g = graph.traversal() - -// Define a property -mgmt = graph.openManagement() -desc = mgmt.makePropertyKey("desc").dataType(String.class).make() -mgmt.commit() - -// Insert some data -graph.addVertex("desc", "foo bar") -graph.addVertex("desc", "foo baz") -graph.tx().commit() - -// Run a query -- note the planner warning recommending the use of an index -g.V().has("desc", containsText("baz")) - -// Create an index -mgmt = graph.openManagement() - -desc = mgmt.getPropertyKey("desc") -mixedIndex = mgmt.buildIndex("mixedExample", Vertex.class).addKey(desc).buildMixedIndex("search") -mgmt.commit() - -// Rollback or commit transactions on the graph which predate the index definition -graph.tx().rollback() - -// Block until the SchemaStatus transitions from INSTALLED to REGISTERED -report = ManagementSystem.awaitGraphIndexStatus(graph, "mixedExample").call() - -// Run a JanusGraph-Hadoop job to reindex -mgmt = graph.openManagement() -mr = new MapReduceIndexManagement(graph) -mr.updateIndex(mgmt.getGraphIndex("mixedExample"), SchemaAction.REINDEX).get() - -// Enable the index -mgmt = graph.openManagement() -mgmt.updateIndex(mgmt.getGraphIndex("mixedExample"), SchemaAction.ENABLE_INDEX).get() -mgmt.commit() - -// Block until the SchemaStatus is ENABLED -mgmt = graph.openManagement() -report = ManagementSystem.awaitGraphIndexStatus(graph, "mixedExample").status(SchemaStatus.ENABLED).call() -mgmt.rollback() - -// Run a query -- JanusGraph will use the new index, no planner warning -g.V().has("desc", containsText("baz")) - -// Concerned that JanusGraph could have read cache in that last query, instead of relying on the index? -// Start a new instance to rule out cache hits. Now we're definitely using the index. -graph.close() -graph = JanusGraphFactory.open("conf/janusgraph-cql-es.properties") -g.V().has("desc", containsText("baz")) ----- - -==== Executing a Reindex job on JanusGraphManagement - -To run a reindex job on JanusGraphManagement, invoke `JanusGraphManagement.updateIndex` with the `SchemaAction.REINDEX` argument. For example: - -[source,gremlin] -m = graph.openManagement() -i = m.getGraphIndex('indexName') -m.updateIndex(i, SchemaAction.REINDEX).get() -m.commit() - -===== Example for JanusGraphManagement - -The following loads some sample data into a BerkeleyDB-backed JanusGraph database, defines an index after the fact, reindexes using JanusGraphManagement, and finally enables and uses the index: - -[source,java] ----- -import org.janusgraph.graphdb.database.management.ManagementSystem - -// Load some data from a file without any predefined schema -graph = JanusGraphFactory.open('conf/janusgraph-berkeleyje.properties') -g = graph.traversal() -m = graph.openManagement() -m.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.LIST).make() -m.makePropertyKey('lang').dataType(String.class).cardinality(Cardinality.LIST).make() -m.makePropertyKey('age').dataType(Integer.class).cardinality(Cardinality.LIST).make() -m.commit() -graph.io(IoCore.gryo()).readGraph('data/tinkerpop-modern.gio') -graph.tx().commit() - -// Run a query -- note the planner warning recommending the use of an index -g.V().has('name', 'lop') -graph.tx().rollback() - -// Create an index -m = graph.openManagement() -m.buildIndex('names', Vertex.class).addKey(m.getPropertyKey('name')).buildCompositeIndex() -m.commit() -graph.tx().commit() - -// Block until the SchemaStatus transitions from INSTALLED to REGISTERED -ManagementSystem.awaitGraphIndexStatus(graph, 'names').status(SchemaStatus.REGISTERED).call() - -// Reindex using JanusGraphManagement -m = graph.openManagement() -i = m.getGraphIndex('names') -m.updateIndex(i, SchemaAction.REINDEX) -m.commit() - -// Enable the index -ManagementSystem.awaitGraphIndexStatus(graph, 'names').status(SchemaStatus.ENABLED).call() - -// Run a query -- JanusGraph will use the new index, no planner warning -g.V().has('name', 'lop') -graph.tx().rollback() - -// Concerned that JanusGraph could have read cache in that last query, instead of relying on the index? -// Start a new instance to rule out cache hits. Now we're definitely using the index. -graph.close() -graph = JanusGraphFactory.open("conf/janusgraph-berkeleyje.properties") -g = graph.traversal() -g.V().has('name', 'lop') ----- - -[[mr-index-removal]] -=== Index Removal - -[WARNING] -Index removal is a manual process comprised of multiple steps. These steps must be carefully followed in the right order to avoid index inconsistencies. - -==== Overview - -Index removal is a two-stage process. In the first stage, one JanusGraph signals to all others via the storage backend that the index is slated for deletion. This changes the index's state to `DISABLED`. At that point, JanusGraph stops using the index to answer queries and stops incrementally updating the index. Index-related data in the storage backend remains present but ignored. - -The second stage depends on whether the index is mixed or composite. A composite index can be deleted via JanusGraph. As with reindexing, removal can be done through either MapReduce or JanusGraphManagement. However, a mixed index must be manually dropped in the index backend; JanusGraph does not provide an automated mechanism to delete an index from its index backend. - -Index removal deletes everything associated with the index except its schema definition and its `DISABLED` state. This schema stub for the index remains even after deletion, though its storage footprint is negligible and fixed. - -==== Preparing for Index Removal - -If the index is currently enabled, it should first be disabled. This is done through the `ManagementSystem`. - -[source,gremlin] -mgmt = graph.openManagement() -rindex = mgmt.getRelationIndex(mgmt.getRelationType("battled"), "battlesByTime") -mgmt.updateIndex(rindex, SchemaAction.DISABLE_INDEX).get() -gindex = mgmt.getGraphIndex("byName") -mgmt.updateIndex(gindex, SchemaAction.DISABLE_INDEX).get() -mgmt.commit() - -Once the status of all keys on the index changes to `DISABLED`, the index is ready to be removed. A utility in ManagementSystem can automate the wait-for-`DISABLED` step: - -[source,gremlin] -ManagementSystem.awaitGraphIndexStatus(graph, 'byName').status(SchemaStatus.DISABLED).call() - -After a composite index is `DISABLED`, there is a choice between two execution frameworks for its removal: - -* MapReduce -* JanusGraphManagement - -Index removal on MapReduce supports large, horizontally-distributed databases. Index removal on JanusGraphManagement spawns a single-machine OLAP job. This is intended for convenience and speed on those databases small enough to be handled by one machine. - -Index removal requires: - -* The index name (a string -- the user provides this to JanusGraph when building a new index) -* The index type (a string -- the name of the edge label or property key on which the vertex-centric index is built). This applies only to vertex-centric indexes - leave blank for global graph indexes. - -As noted in the overview, a mixed index must be manually dropped from the indexing backend. Neither the MapReduce framework nor the JanusGraphManagement framework will delete a mixed backend from the indexing backend. - -==== Executing an Index Removal Job on MapReduce - -As with reindexing, the recommended way to generate and run an index removal job on MapReduce is through the `MapReduceIndexManagement` class. Here is a rough outline of the steps to run an index removal job using this class: - -* Open a `JanusGraph` instance -* If the index has not yet been disabled, disable it through `JanusGraphManagement` -* Pass the graph instance into `MapReduceIndexManagement`'s constructor -* Call `updateIndex(, SchemaAction.REMOVE_INDEX)` - -A commented code example follows in the next subsection. - -===== Example for MapReduce - -[source,java] ----- -import org.janusgraph.graphdb.database.management.ManagementSystem - -// Load the "Graph of the Gods" sample data -graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties') -g = graph.traversal() -GraphOfTheGodsFactory.load(graph) - -g.V().has('name', 'jupiter') - -// Disable the "name" composite index -m = graph.openManagement() -nameIndex = m.getGraphIndex('name') -m.updateIndex(nameIndex, SchemaAction.DISABLE_INDEX).get() -m.commit() -graph.tx().commit() - -// Block until the SchemaStatus transitions from INSTALLED to REGISTERED -ManagementSystem.awaitGraphIndexStatus(graph, 'name').status(SchemaStatus.DISABLED).call() - -// Delete the index using MapReduceIndexJobs -m = graph.openManagement() -mr = new MapReduceIndexManagement(graph) -future = mr.updateIndex(m.getGraphIndex('name'), SchemaAction.REMOVE_INDEX) -m.commit() -graph.tx().commit() -future.get() - -// Index still shows up in management interface as DISABLED -- this is normal -m = graph.openManagement() -idx = m.getGraphIndex('name') -idx.getIndexStatus(m.getPropertyKey('name')) -m.rollback() - -// JanusGraph should issue a warning about this query requiring a full scan -g.V().has('name', 'jupiter') ----- - -==== Executing an Index Removal job on JanusGraphManagement - -To run an index removal job on JanusGraphManagement, invoke `JanusGraphManagement.updateIndex` with the `SchemaAction.REMOVE_INDEX` argument. For example: - -[source,gremlin] -m = graph.openManagement() -i = m.getGraphIndex('indexName') -m.updateIndex(i, SchemaAction.REMOVE_INDEX).get() -m.commit() - -===== Example for JanusGraphManagement - -The following loads some indexed sample data into a BerkeleyDB-backed JanusGraph database, then disables and removes the index through JanusGraphManagement: - -[source,java] ----- -import org.janusgraph.graphdb.database.management.ManagementSystem - -// Load the "Graph of the Gods" sample data -graph = JanusGraphFactory.open('conf/janusgraph-cql-es.properties') -g = graph.traversal() -GraphOfTheGodsFactory.load(graph) - -g.V().has('name', 'jupiter') - -// Disable the "name" composite index -m = graph.openManagement() -nameIndex = m.getGraphIndex('name') -m.updateIndex(nameIndex, SchemaAction.DISABLE_INDEX).get() -m.commit() -graph.tx().commit() - -// Block until the SchemaStatus transitions from INSTALLED to REGISTERED -ManagementSystem.awaitGraphIndexStatus(graph, 'name').status(SchemaStatus.DISABLED).call() - -// Delete the index using JanusGraphManagement -m = graph.openManagement() -nameIndex = m.getGraphIndex('name') -future = m.updateIndex(nameIndex, SchemaAction.REMOVE_INDEX) -m.commit() -graph.tx().commit() - -future.get() - -m = graph.openManagement() -nameIndex = m.getGraphIndex('name') - -g.V().has('name', 'jupiter') ----- - - -=== Common Problems with Index Management - -==== IllegalArgumentException when starting job - -When a reindexing job is started shortly after a the index has been built, the job might fail with an exception like one of the following: - -[source,txt] -The index mixedExample is in an invalid state and cannot be indexed. -The following index keys have invalid status: desc has status INSTALLED -(status must be one of [REGISTERED, ENABLED]) - -[source,txt] -The index mixedExample is in an invalid state and cannot be indexed. -The index has status INSTALLED, but one of [REGISTERED, ENABLED] is required - -When an index is built, its existence is broadcast to all other JanusGraph instances in the cluster. Those must acknowledge the existence of the index before the reindexing process can be started. The acknowledgments can take a while to come in depending on the size of the cluster and the connection speed. Hence, one should wait a few minutes after building the index and before starting the reindex process. - -Note, that the acknowledgment might fail due to JanusGraph instance failure. In other words, the cluster might wait indefinitely on the acknowledgment of a failed instance. In this case, the user must manually remove the failed instance from the cluster registry as described in <>. After the cluster state has been restored, the acknowledgment process must be reinitiated by manually registering the index again in the management system. - -[source,gremlin] -mgmt = graph.openManagement() -rindex = mgmt.getRelationIndex(mgmt.getRelationType("battled"),"battlesByTime") -mgmt.updateIndex(rindex, SchemaAction.REGISTER_INDEX).get() -gindex = mgmt.getGraphIndex("byName") -mgmt.updateIndex(gindex, SchemaAction.REGISTER_INDEX).get() -mgmt.commit() - -After waiting a few minutes for the acknowledgment to arrive the reindex job should start successfully. - -==== Could not find index - -This exception in the reindexing job indicates that an index with the given name does not exist or that the name has not been specified correctly. When reindexing a global graph index, only the name of the index as defined when building the index should be specified. When reindexing a global graph index, the name of the index must be given in addition to the name of the edge label or property key on which the vertex-centric index is defined. - -==== Cassandra Mappers Fail with "Too many open files" - -The end of the exception stacktrace may look like this: - ----- -java.net.SocketException: Too many open files - at java.net.Socket.createImpl(Socket.java:447) - at java.net.Socket.getImpl(Socket.java:510) - at java.net.Socket.setSoLinger(Socket.java:988) - at org.apache.thrift.transport.TSocket.initSocket(TSocket.java:118) - at org.apache.thrift.transport.TSocket.(TSocket.java:109) ----- - -When running Cassandra with virtual nodes enabled, the number of virtual nodes seems to set a floor under the number of mappers. Cassandra may generate more mappers than virtual nodes for clusters with lots of data, but it seems to generate at least as many mappers as there are virtual nodes even though the cluster might be empty or close to empty. The default is 256 as of this writing. - -Each mapper opens and quickly closes several sockets to Cassandra. The kernel on the client side of those closed sockets goes into asynchronous TIME_WAIT, since Thrift uses SO_LINGER. Only a small number of sockets are open at any one time -- usually low single digits -- but potentially many lingering sockets can accumulate in TIME_WAIT. This accumulation is most pronounced when running a reindex job locally (not on a distributed MapReduce cluster), since all of those client-side TIME_WAIT sockets are lingering on a single client machine instead of being spread out across many machines in a cluster. Combined with the floor of 256 mappers, a reindex job can open thousands of sockets of the course of its execution. When these sockets all linger in TIME_WAIT on the same client, they have the potential to reach the open-files ulimit, which also controls the number of open sockets. The open-files ulimit is often set to 1024. - -Here are a few suggestions for dealing with the "Too many open files" problem during reindexing on a single machine: - -* Reduce the maximum size of the Cassandra connection pool. For example, consider setting the cassandrathrift storage backend's `max-active` and `max-idle` options to 1 each, and setting `max-total` to -1. See <> for full listings of connection pool settings on the Cassandra storage backends. -* Increase the `nofile` ulimit. The ideal value depends on the size of the Cassandra dataset and the throughput of the reindex mappers; if starting at 1024, try an order of magnitude larger: 10000. This is just necessary to sustain lingering TIME_WAIT sockets. The reindex job won't try to open nearly that many sockets at once. -* Run the reindex task on a multi-node MapReduce cluster to spread out the socket load. diff --git a/docs/serializer.adoc b/docs/serializer.adoc deleted file mode 100644 index 7d5cc44bb2..0000000000 --- a/docs/serializer.adoc +++ /dev/null @@ -1,27 +0,0 @@ -[[serializer]] -== Datatype and Attribute Serializer Configuration - -JanusGraph supports a number of classes for attribute values on properties. JanusGraph efficiently serializes primitives, primitive arrays and `Geoshape`, `UUID`, and `Date`. JanusGraph supports serializing arbitrary objects as attribute values, but these require custom serializers to be defined. - -To configure a custom attribute class with a custom serializer, follow these steps: - -. Implement a custom `AttributeSerializer` for the custom attribute class -. Add the following configuration options where [X] is the custom attribute id that must be larger than all attribute ids for already configured custom attributes: -.. `attributes.custom.attribute[X].attribute-class = [Full attribute class name]` -.. `attributes.custom.attribute[X].serializer-class = [Full serializer class name]` - -For example, suppose we want to register a special integer attribute class called `SpecialInt` and have implemented a custom serializer `SpecialIntSerializer` that implements `AttributeSerializer`. We already have 9 custom attributes configured in the configuration file, so we would add the following lines -``` -attributes.custom.attribute10.attribute-class = com.example.SpecialInt -attributes.custom.attribute10.serializer-class = com.example.SpecialIntSerializer -``` - -=== Custom Object Serialization - -JanusGraph supports arbitrary objects as property attributes and can serialize such objects to disk. For this default serializer to work for a custom class, the following conditions must be fulfilled: - -* The class must implement AttributeSerializer -* The class must have a no-argument constructor -* The class must implement the `equals(Object)` method - -The last requirement is needed because JanusGraph will test both serialization and deserialization of a custom class before persisting data to disk. diff --git a/docs/solr.adoc b/docs/solr.adoc deleted file mode 100644 index 2c488a0c95..0000000000 --- a/docs/solr.adoc +++ /dev/null @@ -1,441 +0,0 @@ -[[solr]] -== Apache Solr - - -[quote, 'http://lucene.apache.org/solr/[Apache Solr Homepage]'] -Solr is the popular, blazing fast open source enterprise search platform from the Apache Lucene project. Solr is a standalone enterprise search server with a REST-like API. Solr is highly reliable, scalable and fault tolerant, providing distributed indexing, replication and load-balanced querying, automated failover and recovery, centralized configuration and more. - -JanusGraph supports http://lucene.apache.org/solr/[Apache Solr] as an index backend. Here are some of the Solr features supported by JanusGraph: - -* *Full-Text*: Supports all `Text` predicates to search for text properties that matches a given word, prefix or regular expression. -* *Geo*: Supports all `Geo` predicates to search for geo properties that are intersecting, within, disjoint to or contained in a given query geometry. Supports points, lines and polygons for indexing. Supports circles, boxes and polygons for querying point properties and all shapes for querying non-point properties. -* *Numeric Range*: Supports all numeric comparisons in `Compare`. -* *TTL*: Supports automatically expiring indexed elements. -* *Temporal*: Millisecond granularity temporal indexing. -* *Custom Analyzer*: Choose to use a custom analyzer - -Please see <> for details on what versions of Solr will work with JanusGraph. - - -=== Solr Configuration Overview - -JanusGraph supports Solr running in either a SolrCloud or Solr Standalone (HTTP) configuration for use with a *mixed index* (see <>). The desired connection mode is configured via the parameter `mode` which must be set to either `cloud` or `http`, the former being the default value. For example, to explicitly specify that Solr is running in a SolrCloud configuration the following property is specified as a JanusGraph configuration property: - -[source, properties] ----- -index.search.solr.mode=cloud ----- - -These are some key Solr terms: - -* *Core*: A 'single index' on a single machine - -* *Configuration*: 'solrconfig.xml', 'schema.xml', and other files required to define a core. - -* *Collection*: A 'single logical index' that can span multiple cores on different machines. - -* *Configset*: A shared 'configuration' that can be reused by multiple cores. - - -=== Connecting to SolrCloud - -When connecting to a SolrCloud cluster by setting the `mode` equal to `cloud`, the Zookeeper URL (and optionally port) must be specified so that JanusGraph can discover and interact with the Solr cluster. - -[source, properties] ----- -index.search.backend=solr -index.search.solr.mode=cloud -index.search.solr.zookeeper-url=localhost:2181 ----- - -A number of additional configuration options pertaining to the creation of new collections (which is only supported in SolrCloud operation mode) can be configured to control sharding behavior among other things. Refer to the <> for a complete listing of those options. - -SolrCloud leverages Zookeeper to coordinate collection and configset information between the Solr servers. The use of Zookeeper with SolrCloud provides the opportunity to significantly reduce the amount of manual configuration required to use Solr as a back end index for JanusGraph. - - -==== Configset Configuration - -A configset is required to create a collection. The configset is stored in Zookeeper to enable access to it across the Solr servers. - -* Each collection can provide its own configset when it is created, so that each collection may have a different configuration. With this approach, each collection must be created manually. -* A shared configset can be uploaded separately to Zookeeper if it will be reused by multiple collections. With this approach, JanusGraph can create collections automatically by using the shared configset. Another benefit is that reusing a configset significantly reduces the amount of data stored in Zookeeper. - - -===== Using an Individual Configset - -In this example, a collection named `verticesByAge` is created manually using the default JanusGraph configuration for Solr that is found in the distribution. When the collection is created, the configuration is uploaded into Zookeeper, using the same collection name `verticesByAge` for the configset name. Refer to the https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-CollectionsandCores[Solr Reference Guide] for available parameters. - -[source, bourne] ----- -# create the collection -$SOLR_HOME/bin/solr create -c verticesByAge -d $JANUSGRAPH_HOME/conf/solr ----- - -Define a mixed index using `JanusGraphManagement` and the same collection name. - -[source, gremlin] ----- -mgmt = graph.openManagement() -age = mgmt.makePropertyKey("age").dataType(Integer.class).make() -mgmt.buildIndex("verticesByAge", Vertex.class).addKey(age).buildMixedIndex("search") -mgmt.commit() ----- - - -===== Using a Shared Configset - -When using a shared configset, it is most convenient to upload the configuration first as a one time operation. In this example, a configset named `janusgraph-configset` is uploaded in to Zookeeper using the default JanusGraph configuration for Solr that is found in the distribution. Refer to the https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-CollectionsandCores[Solr Reference Guide] for available parameters. - -[source, bourne] ----- -# upload the shared configset into Zookeeper -# Solr 5 -$SOLR_HOME/server/scripts/cloud-scripts/zkcli.sh -cmd upconfig -z localhost:2181 \ - -d $JANUSGRAPH_HOME/conf/solr -n janusgraph-configset -# Solr 6 and higher -$SOLR_HOME/bin/solr zk upconfig -d $JANUSGRAPH_HOME/conf/solr -n janusgraph-configset \ - -z localhost:2181 ----- - -When configuring the SolrCloud indexing backend for JanusGraph, make sure to provide the name of the shared configset using the `index.search.solr.configset` property. - -[source, properties] ----- -index.search.backend=solr -index.search.solr.mode=cloud -index.search.solr.zookeeper-url=localhost:2181 -index.search.solr.configset=janusgraph-configset ----- - -Define a mixed index using `JanusGraphManagement` and the collection name. - -[source, gremlin] ----- -mgmt = graph.openManagement() -age = mgmt.makePropertyKey("age").dataType(Integer.class).make() -mgmt.buildIndex("verticesByAge", Vertex.class).addKey(age).buildMixedIndex("search") -mgmt.commit() ----- - - -=== Connecting to Solr Standalone (HTTP) - -When connecting to Solr Standalone via HTTP by setting the `mode` equal to `http`, a single or list of URLs for the Solr instances must be provided. - -[source, properties] ----- -index.search.backend=solr -index.search.solr.mode=http -index.search.solr.http-urls=http://localhost:8983/solr ----- - -Additional configuration options for controlling the maximum number of connections, connection timeout and transmission compression are available for the HTTP mode. Refer to the <> for a complete listing of those options. - - -==== Core Configuration - -Solr Standalone is used for a single instance, and it keeps configuration information on the file system. A core must be created manually for each mixed index. - -To create a core, a `core_name` and a `configuration` directory is required. Refer to the https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-CollectionsandCores[Solr Reference Guide] for available parameters. In this example, a core named `verticesByAge` is created using the default JanusGraph configuration for Solr that is found in the distribution. - -[source, bourne] ----- -$SOLR_HOME/bin/solr create -c verticesByAge -d $JANUSGRAPH_HOME/conf/solr ----- - -Define a mixed index using `JanusGraphManagement` and the same core name. - -[source, gremlin] ----- -mgmt = graph.openManagement() -age = mgmt.makePropertyKey("age").dataType(Integer.class).make() -mgmt.buildIndex("verticesByAge", Vertex.class).addKey(age).buildMixedIndex("search") -mgmt.commit() ----- - - -=== Solr Schema Design - -==== Dynamic Field Definition - -By default, JanusGraph uses Solr's https://cwiki.apache.org/confluence/display/solr/Dynamic+Fields[Dynamic Fields] feature to define the field types for all indexed keys. This requires no extra configuration when adding property keys to a mixed index backed by Solr and provides better performance than schemaless mode. - -JanusGraph assumes the following dynamic field tags are defined in the backing Solr collection's schema.xml file. Please note that there -is additional xml definition of the following fields required in a solr schema.xml file in order to use them. Reference the example schema.xml file provided in the ./conf/solr/schema.xml directory in a JanusGraph installation for more information. - -[source, xml] ----- - - - - - - - - - - - ----- - -In JanusGraph's default configuration, property key names do not have to end with the type-appropriate suffix to take advantage of Solr's dynamic field feature. JanusGraph generates the Solr field name from the property key name by encoding the property key definition's numeric identifier and the type-appropriate suffix. This means that JanusGraph uses synthetic field names with type-appropriate suffixes behind the scenes, regardless of the property key names defined and used by application code using JanusGraph. This field name mapping can be overridden through non-default configuration. That's described in the next section. - -==== Manual Field Definition - -If the user would rather manually define the field types for each of the indexed fields in a collection, the configuration option `dyn-fields` needs to be disabled. It is important that the field for each indexed property key is defined in the backing Solr schema before the property key is added to the index. - -In this scenario, it is advisable to enable explicit property key name to field mapping in order to fix the field names for their explicit definition. This can be achieved in one of two ways: - -. Configuring the name of the field by providing a `mapped-name` parameter when adding the property key to the index. See <> for more information. -. By enabling the `map-name` configuration option for the Solr index which will use the property key name as the field name in Solr. See <> for more information. - -==== Schemaless Mode - -JanusGraph can also interact with a SolrCloud cluster that is configured for https://cwiki.apache.org/confluence/display/solr/Schemaless+Mode[schemaless mode]. In this scenario, the configuration option `dyn-fields` should be disabled since Solr will infer the field type from the values and not the field name. - -Note, however, that schemaless mode is recommended only for prototyping and initial application development and NOT recommended for production use. - - -=== Troubleshooting - -==== Collection Does Not Exist - -The collection (and all of the required configuration files) must be initialized before a defined index can use the collection. See <> for more information. - -When using SolrCloud, the Zookeeper zkCli.sh command line tool can be used to inspect the configurations loaded into Zookeeper. Also verify that -the default JanusGraph configuration files are copied to the correct location under solr and that the directory where the files are copied is correct. - -==== Cannot Find the Specified Configset - -When using SolrCloud, a configset is required to create a mixed index for JanusGraph. See <> for more information. - -* If using an individual configset, the collection must be created manually first. -* If using a shared configset, the configset must be uploaded into Zookeeper first. - -You can verify that the configset and its configuration files are in Zookeeper under `/configs`. Refer to the https://lucene.apache.org/solr/guide/6_6/solr-control-script-reference.html#SolrControlScriptReference-ZooKeeperOperations[Solr Reference Guide] for other Zookeeper operations. - -[source, bourne] ----- -# verify the configset in Zookeeper -# Solr 5 -$SOLR_HOME/server/scripts/cloud-scripts/zkcli.sh -cmd list -z localhost:2181 -# Solr 6 and higher -$SOLR_HOME/bin/solr zk ls -r /configs/configset-name -z localhost:2181 ----- - -==== HTTP Error 404 - -This error may be encountered when using Solr Standalone (HTTP) mode. An example of the error: - -[source, text] ----- -20:01:22 ERROR org.janusgraph.diskstorage.solr.SolrIndex - Unable to save documents -to Solr as one of the shape objects stored were not compatible with Solr. -org.apache.solr.client.solrj.impl.HttpSolrClient$RemoteSolrException: Error from server -at http://localhost:8983/solr: Expected mime type application/octet-stream but got text/html. - - - -Error 404 Not Found - -

HTTP ERROR 404

-

Problem accessing /solr/verticesByAge/update. Reason: -

    Not Found

- - ----- - -Make sure to create the core manually before attempting to store data into the index. See <> for more information. - -==== Invalid core or collection name - -The core or collection name is an identifier. It must consist entirely of periods, underscores, hyphens, and/or alphanumerics, and also it may not start with a hyphen. - -==== Connection Problems - -Irrespective of the operation mode, a Solr instance or a cluster of Solr instances must be running and accessible from the JanusGraph instance(s) in order for JanusGraph to use Solr as an indexing backend. Check that the Solr cluster is running correctly and that it is visible and accessible over the network (or locally) from the JanusGraph instances. - -==== JTS ClassNotFoundException with Geo Data - -Solr relies on Spatial4j for geo processing. Spatial4j declares an optional dependency on JTS ("JTS Topology Suite"). JTS is required for some geo field definition and query functionality. If the JTS jar is not on the Solr daemon's classpath and a field in schema.xml uses a geo type, then Solr may throw a ClassNotFoundException on one of the missing JTS classes. The exception can appear when starting Solr using a schema.xml file designed to work with JanusGraph, but can also appear when invoking `CREATE` in the https://wiki.apache.org/solr/CoreAdmin[Solr CoreAdmin API]. The exception appears in slightly different formats on the client and server sides, although the root cause is identical. - -Here's a representative example from a Solr server log: - -[source, text] ----- -ERROR [http-8983-exec-5] 2014-10-07 02:54:06, 665 SolrCoreResourceManager.java (line 344) com/vividsolutions/jts/geom/Geometry -java.lang.NoClassDefFoundError: com/vividsolutions/jts/geom/Geometry - at com.spatial4j.core.context.jts.JtsSpatialContextFactory.newSpatialContext(JtsSpatialContextFactory.java:30) - at com.spatial4j.core.context.SpatialContextFactory.makeSpatialContext(SpatialContextFactory.java:83) - at org.apache.solr.schema.AbstractSpatialFieldType.init(AbstractSpatialFieldType.java:95) - at org.apache.solr.schema.AbstractSpatialPrefixTreeFieldType.init(AbstractSpatialPrefixTreeFieldType.java:43) - at org.apache.solr.schema.SpatialRecursivePrefixTreeFieldType.init(SpatialRecursivePrefixTreeFieldType.java:37) - at org.apache.solr.schema.FieldType.setArgs(FieldType.java:164) - at org.apache.solr.schema.FieldTypePluginLoader.init(FieldTypePluginLoader.java:141) - at org.apache.solr.schema.FieldTypePluginLoader.init(FieldTypePluginLoader.java:43) - at org.apache.solr.util.plugin.AbstractPluginLoader.load(AbstractPluginLoader.java:190) - at org.apache.solr.schema.IndexSchema.readSchema(IndexSchema.java:470) - at com.datastax.bdp.search.solr.CassandraIndexSchema.readSchema(CassandraIndexSchema.java:72) - at org.apache.solr.schema.IndexSchema.(IndexSchema.java:168) - at com.datastax.bdp.search.solr.CassandraIndexSchema.(CassandraIndexSchema.java:54) - at com.datastax.bdp.search.solr.core.CassandraCoreContainer.create(CassandraCoreContainer.java:210) - at com.datastax.bdp.search.solr.core.SolrCoreResourceManager.createCore(SolrCoreResourceManager.java:256) - at com.datastax.bdp.search.solr.handler.admin.CassandraCoreAdminHandler.handleCreateAction(CassandraCoreAdminHandler.java:117) - ... ----- - -Here's what normally appears in the output of the client that issued the associated `CREATE` command to the CoreAdmin API: - -[source, text] ----- -org.apache.solr.common.SolrException: com/vividsolutions/jts/geom/Geometry - at com.datastax.bdp.search.solr.core.SolrCoreResourceManager.createCore(SolrCoreResourceManager.java:345) - at com.datastax.bdp.search.solr.handler.admin.CassandraCoreAdminHandler.handleCreateAction(CassandraCoreAdminHandler.java:117) - at org.apache.solr.handler.admin.CoreAdminHandler.handleRequestBody(CoreAdminHandler.java:152) - ... ----- - -This is resolved by adding the JTS jar to the classpath of JanusGraph and/or the Solr server. JTS is not included in JanusGraph distributions by default due to its LGPL license. Users must download the http://search.maven.org/remotecontent?filepath=com/vividsolutions/jts/1.13/jts-1.13.jar[JTS jar file] separately and copy it into the JanusGraph and/or Solr server lib directory. If using Solr's built in web server, the JTS jar may be copied to the example/solr-webapp/webapp/WEB-INF/lib directory to include it in the classpath. Solr can be restarted, and the exception should be gone. Solr must be started once with the correct schema.xml file in place first, for the example/solr-webapp/webapp/WEB-INF/lib directory to exist. - -To determine the ideal JTS version for Solr server, first check the version of Spatial4j in use by the Solr cluster, then determine the version of JTS against which that Spatial4j version was compiled. Spatial4j declares its target JTS version in the http://search.maven.org/#search|gav|1|g%3A%22com.spatial4j%22%20AND%20a%3A%22spatial4j%22[pom for the `com.spatial4j:spatial4j` artifact]. -Copy the JTS jar to the server/solr-webapp/webapp/WEB-INF/lib directory in your solr installation. - -=== Advanced Solr Configuration - -[[dse-search]] -==== DSE Search - -This section covers installation and configuration of JanusGraph with DataStax Enterprise (DSE) Search. There are multiple ways to install DSE, but this section focuses on DSE's binary tarball install option on Linux. Most of the steps in this section can be generalized to the other install options for DSE. - -Install DataStax Enterprise as directed by the page http://www.datastax.com/documentation/datastax_enterprise/4.5/datastax_enterprise/install/installTARdse.html[Installing DataStax Enterprise using the binary tarball]. - -Export `DSE_HOME` and append to `PATH` in your shell environment. Here's an example using Bash syntax: - -[source, bourne] -export DSE_HOME=/path/to/dse-version.number -export PATH="$DSE_HOME"/bin:"$PATH" - -Install JTS for Solr. The appropriate version varies with the Spatial4j version. As of DSE 4.5.2, the appropriate version is 1.13. - -[source, bourne] ----- -cd $DSE_HOME/resources/solr/lib -curl -O 'http://central.maven.org/maven2/com/vividsolutions/jts/1.13/jts-1.13.jar' ----- - -Start DSE Cassandra and Solr in a single background daemon: - -[source, bourne] ----- -# The "dse-data" path below was chosen to match the -# "Installing DataStax Enterprise using the binary tarball" -# documentation page from DataStax. The exact path is not -# significant. -dse cassandra -s -Ddse.solr.data.dir="$DSE_HOME"/dse-data/solr ----- - -The previous command will write some startup information to the console and to the logfile path `log4j.appender.R.File` configured in `$DSE_HOME/resources/cassandra/conf/log4j-server.properties`. - -Once DSE with Cassandra and Solr has started normally, check the cluster health with `nodetool status`. A single-instance ring should show one node with flags *U*p and *N*ormal: - -[source, bourne] ----- -nodetool status -Note: Ownership information does not include topology; for complete information, specify a keyspace -= Datacenter: Solr -Status=Up/Down -|/ State=Normal/Leaving/Joining/Moving --- Address Load Owns Host ID Token Rack -UN 127.0.0.1 99.89 KB 100.0% 5484ef7b-ebce-4560-80f0-cbdcd9e9f496 -7317038863489909889 rack1 ----- - -Next, switch to Gremlin Console and open a JanusGraph database against the DSE instance. This will create JanusGraph's keyspace and column families. -[source, gremlin] ----- -cd $JANUSGRAPH_HOME -bin/gremlin.sh - - \,,,/ - (o o) ------oOOo-(3)-oOOo----- -gremlin> graph = JanusGraphFactory.open('conf/janusgraph-cql-solr.properties') -==>janusgraph[cql:[127.0.0.1]] -gremlin> g = graph.traversal() -==>graphtraversalsource[janusgraph[cql:[127.0.0.1]], standard] -gremlin> ----- - -Keep this Gremlin Console open. We'll take a break now to install -a Solr core. Then we'll come back to this console to load some sample -data. - -Next, upload configuration files for JanusGraph's Solr collection, then -create the core in DSE: - -[source, bourne] ----- -# Change to the directory where JanusGraph was extracted. Later commands -# use relative paths to the Solr config files shipped with the JanusGraph -# distribution. -cd $JANUSGRAPH_HOME - -# The name must be URL safe and should contain one dot/full-stop -# character. The part of the name after the dot must not conflict with -# any of JanusGraph's internal CF names. Starting the part after the dot -# "solr" will avoid a conflict with JanusGraph's internal CF names. -CORE_NAME=janusgraph.solr1 -# Where to upload collection configuration and send CoreAdmin requests. -SOLR_HOST=localhost:8983 - -# The value of index.[X].solr.http-urls in JanusGraph's config file -# should match $SOLR_HOST and $CORE_NAME. For example, given the -# $CORE_NAME and $SOLR_HOST values above, JanusGraph's config file would -# contain (assuming "search" is the desired index alias): -# -# index.search.solr.http-urls=http://localhost:8983/solr/janusgraph.solr1 -# -# The stock JanusGraph config file conf/janusgraph-cql-solr.properties -# ships with this http-urls value. - -# Upload Solr config files to DSE Search daemon -for xml in conf/solr/{solrconfig, schema, elevate}.xml ; do - curl -v http://"$SOLR_HOST"/solr/resource/"$CORE_NAME/$xml" \ - --data-binary @"$xml" -H 'Content-type:text/xml; charset=utf-8' -done -for txt in conf/solr/{protwords, stopwords, synonyms}.txt ; do - curl -v http://"$SOLR_HOST"/solr/resource/"$CORE_NAME/$txt" \ - --data-binary @"$txt" -H 'Content-type:text/plain; charset=utf-8' -done -sleep 5 - -# Create core using the Solr config files just uploaded above -curl "http://"$SOLR_HOST"/solr/admin/cores?action=CREATE&name=$CORE_NAME" -sleep 5 - -# Retrieve and print the status of the core we just created -curl "http://localhost:8983/solr/admin/cores?action=STATUS&core=$CORE_NAME" ----- - -Now the JanusGraph database and backing Solr core are ready for use. We -can test it out with the <> -dataset. Picking up the Gremlin Console session started above: - -[source, gremlin] ----- -// Assuming graph = JanusGraphFactory.open('conf/janusgraph-cql-solr.properties')... -gremlin> GraphOfTheGodsFactory.load(graph) -==>null ----- - -Now we can run any of the queries described in <>. -Queries involving text and geo predicates will be served by Solr. For -more verbose reporting from JanusGraph and the Solr client, run `gremlin.sh --l DEBUG` and issue some index-backed queries. - -/////////// - -==== Hadoop Search - -tbw - -/////////// diff --git a/docs/static/css/docs.css b/docs/static/css/docs.css deleted file mode 100644 index 22c704fa0c..0000000000 --- a/docs/static/css/docs.css +++ /dev/null @@ -1,397 +0,0 @@ -/* This file collects customizations made to the theme - during the development of the AsciiDoc/DocBook-based manual pages. */ - -#header-sticky .logo a, #header .logo a { height: auto; } - -/* Break very wide default values */ -.font16 + .informaltable td:nth-child(4) { - word-break: break-all; - min-width: 25%; -} - -.header-wrapper { - overflow: hidden; - position: fixed; - top: 0; - width: 100%; - background-color: #FFFFFF; - border-bottom: 1px dotted #1b8669; -} -.header-list { - list-style-type: none; - margin: 0; - padding: 0; - overflow: hidden; -} -.header-item { - float: left; -} -.header-item-right { - float: right; - line-height: 67px; - padding-left: 15px; - padding-right: 15px; -} -.header-item-right a:hover { - color: #1b8669; - text-decoration: none; - border-bottom: 3px solid #1b8669; -} - -/* Static anchor offset to compensate for the height of the floating header */ -a[name] { - padding-top: 88px; - margin-top: -88px; - display: inline-block; -} - -.navheader { - background-color: #F0F0F0; - padding: 6px 30px; - border-top: 1px solid black; - border-bottom: 1px dotted #1b8669; -} - -.font16 { - font-size: 16px; -} - -div#main { - /* padding-left: 30px; - padding-right: 30px; */ - margin-top: 67px; - margin-left: auto; - margin-right: auto; - padding-top: 10px; - max-width: 940px; -} - -div#main > div.chapter > div.toc { - float: right; - margin-left: 25px; -} - -div#main > div.chapter > div.section .title { - clear: none !important; -} - -div.note, div.tip, div.important, div.warning, div.caution { - margin: 12px 18px; - padding: 12px 18px; - border: 1px dotted #A4A4A4; -} - -.breadcrumbs { - margin-bottom: 5px; -} - -.hl-number { - color: #A050A0; -} - -.hl-string { - color: #000080; -} - -.hl-keyword { - color: #A02050; -} - -.hl-attribute { - color: rgb(225,7,7); -} - -.hl-comment { - color: #007000; -} - -.hl-repl-prompt { - color: #44AA44; - font-weight: bold; -} - -.hl-repl-result { - color: #AAAAAA; - font-weight: bold; -} - -.hl-gremlin-func { - color: #B04040; -} - -.figure img, -.figure > .title { - margin-left: auto; - margin-right: auto; -} - -.tss-center { - display: block; -} - -.tss-center img { - display: block; - margin-left: auto; - margin-right: auto; -} - -.tss-width-125 img { - max-width: 125px; -} - -.tss-width-250 img { - max-width: 250px; -} - -.tss-floatright { - float: right; - margin-left: 15px; -} - -.tss-floatleft { - float: left; - margin-right: 15px; -} - -.figure img { - display: block; -} - -div.informaltable > table { - margin: 10px auto; - display: table; -} - -div.informaltable thead { - background-color: #EEEEEE; -} - -.programlisting { - padding: 5px 8px; - border: 1px dashed #1b8669; - color: #000000; - background-color: #F8F8F8; - overflow-x: auto; -} - -div#main { - /* padding-left: 30px; - padding-right: 30px; */ - margin-left: auto; - margin-right: auto; - max-width: 940px; -} - -.titlepage hr, .navheader hr { - display: none; -} - -.navfooter { - background-color: #F0F0F0; - padding: 6px 30px; - border-top: 1px dotted #1b8669; -} - -.navfooter table { - margin-left: auto; - margin-right: auto; - max-width: 940px; -} - -.navfooter tbody { - font-size: 14px; -} - -.navfooter hr { - display: none; -} - -.title { - overflow: auto; -} -h1.title { - line-height: normal; -} -h2.title { - font-size: 18px; -} - -.line-through { - text-decoration: line-through; -} - -.deprecation-warning { - color: #E10707; -} - -html { - background-color: #000000; -} - -.tss-centered > .inlinemediaobject > * { - margin-left: auto; - margin-right: auto; - display: block; -} - -.tss-centered > .mediaobject > * { - margin-left: auto; - margin-right: auto; - display: block; -} - -/* Change header font */ - -#main .reading-box h2, -#main h2, -.page-title h1, -.image .image-extras .image-extras-content h3,.image .image-extras .image-extras-content h3 a, -#main .post h2, -#sidebar .widget h3, -.tab-holder .tabs li a, -.share-box h4, -.project-content h3, -.author .author_title, -h5.toggle a, -.full-boxed-pricing ul li.title-row, -.full-boxed-pricing ul li.pricing-row, -.sep-boxed-pricing ul li.title-row, -.sep-boxed-pricing ul li.pricing-row, -.person-author-wrapper, -.post-content h1, .post-content h2, .post-content h3, .post-content h4, .post-content h5, .post-content h6, -.ei-title h2, #header-sticky,#header .tagline, -table th,.project-content .project-info h4, -.woocommerce-success-message .msg,.product-title, .cart-empty, -#wrapper .fusion-megamenu-wrapper .fusion-megamenu-title{ -font-family:"Open Sans", Arial, Helvetica, sans-serif !important; -} - -table.blockquote { - width: auto !important; -} - -.footer-area h3,#slidingbar-area h3{ -font-family:"Open Sans", Arial, Helvetica, sans-serif !important; -} - -.footer-wrapper { - background-color: #282a2b; - overflow: hidden; -} -#footer { - max-width: 940px; - margin-left: auto; - margin-right: auto; - height: auto; -} -.copyright { - padding-top: 10px; - padding-bottom: 12px; - float:left; - color:#8C8989; - line-height: 20px; - font-size:12px; - text-shadow:1px 2px 1px #000; -} -.copyright a {color:#bfbfbf;} - -/* Change title size and font */ - -#main h1 { - margin: 0 0 31px; -} - -#main h2 { - font-size: 28px/30px; - font-weight: bold; - font-family:"Open Sans", Arial, Helvetica, sans-serif !important; - margin: 0 0 20px; -} - -/*Make text black */ -body,.post .post-content,.post-content blockquote,.tab-holder .news-list li .post-holder .meta,#sidebar #jtwt,.meta,.review blockquote div,.search input,.project-content .project-info h4,.title-row,.simple-products-slider .price .amount, -.quantity .qty,.quantity .minus,.quantity .plus,.blog-shortcode h3.timeline-title, #reviews #comments > h2{ -color:#000000 !important; -} - -/* Change text size and line height */ -body, #sidebar .slide-excerpt h2, .footer-area .slide-excerpt h2, #slidingbar-area .slide-excerpt h2 { -font-family: "Lato", Arial, Helvetica, sans-serif; -font-size: 15px; -line-height: 22px; -} - -body { - margin: 0; -} -body a { - color: #333333; - text-decoration: none; -} -body a:hover { - color:#3030FF; - text-decoration: underline; -} - -/* Make note font color grey */ -/* .note p, .note th { - color: #747474 !important; -} */ - -/* Make toc have background color and adjust padding */ -div#main > div.chapter > div.toc { -margin-left: 15px; -padding: 10px; -background-color: #EEE; -} - -/* Change link color */ -a.link,a.xref { - color:#3030FF !important; -} -a.link:hover { - color:#3030FF !important; - text-decoration: underline; -} - -/* The container
- needed to position the dropdown content */ -.dropdown { - position: relative; - display: inline-block; -} - -/* Dropdown Content (Hidden by Default) */ -.dropdown-content { - display: none; - position: absolute; - background-color: #f1f1f1; - width: 100%; - overflow: auto; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 1; -} - -/* Links inside the dropdown */ -.dropdown-content a { - color: black; - padding: 12px 16px; - text-decoration: none; - display: block; - line-height: 30px; -} - -/* Change color of dropdown links on hover */ -.dropdown-content a:hover {background-color: #ddd} - -/* Show the dropdown menu on hover */ -.dropdown:hover .dropdown-content { - display: block; -} - -.header-wrapper:hover { - overflow: visible; -} - -.header-list:hover { - overflow: visible; -} diff --git a/docs/static/favicon.ico b/docs/static/favicon.ico deleted file mode 100644 index 43d2fb6ff8ebf05c33f01e9ed83575e6d0c4732a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24838 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 Rl{Dk!{nCRn>aiGm_%G50!4Lod diff --git a/docs/static/images/icons/README b/docs/static/images/icons/README deleted file mode 100644 index f12b2a730c..0000000000 --- a/docs/static/images/icons/README +++ /dev/null @@ -1,5 +0,0 @@ -Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook -icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency -from the Jimmac icons to get round MS IE and FOP PNG incompatibilies. - -Stuart Rackham diff --git a/docs/static/images/icons/callouts/1.png b/docs/static/images/icons/callouts/1.png deleted file mode 100644 index 7d473430b7bec514f7de12f5769fe7c5859e8c5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQC}X^4DKU-G|w_t}fLBA)Suv#nrW z!^h2QnY_`l!BOq-UXEX{m2up>JTQkX)2m zTvF+fTUlI^nXH#utd~++ke^qgmzgTe~DWM4ffP81J diff --git a/docs/static/images/icons/callouts/10.png b/docs/static/images/icons/callouts/10.png deleted file mode 100644 index 997bbc8246a316e040e0804174ba260e219d7d33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 361 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQWtZ~+OvdJMW|Y+^UT?O-M{rKJsmzxdayJ{ zDCQA!%%@7Jj$q%-wf8e0_jRx8Dqi$}^?K=?6FriQFLv>>oc^CE+aVHhW3=nZ+fQ4!M=ZC7H>3sl|FJr3LwU zC3?yExf6FO?f@F61vV}-Juk7O6lk8Yg;}bFaZ-|HQc7Azopr01?u8M*si- diff --git a/docs/static/images/icons/callouts/11.png b/docs/static/images/icons/callouts/11.png deleted file mode 100644 index ce47dac3f52ac49017749a3fea53db57d006993c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 565 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1SD^YpWXnZI14-?iy0V%N{XE z)7O>#600DeuDZ?5tOl@ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=9KoYUDZ+`aP)jU z&ny=ErrK^#Gw!AcR}pdfMERuV^@&0$@(#^6b8c@rn^6RWX3pUb z4*6@PZ+H0#u=rjsXzS?6n6*sBGbHqGTU%mCsH?n#%j;eD^2}qe=iX*J@VQ3BRpz+u z{PX#N(^9X${`$90+;!pWs>o@z_n8G)7Uo7PJz`jrS+)QE@=PWHmc~UIw=WmUe73o7 z>^bR(M752aYoNg~ozu7U7&{(U>{s!;bn#f?ItjL^o`e{*EOQHqO;ccnz9hLK5@2cAyw@AaPFL~Cp#02|E|4xeQteNtB7waMs QVCXP-y85}Sb4q9e0GRUFb^rhX diff --git a/docs/static/images/icons/callouts/12.png b/docs/static/images/icons/callouts/12.png deleted file mode 100644 index 31daf4e2f25b6712499ee32de9c2e3b050b691ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 617 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1SD^YpWXnZI14-?iy0V%N{XE z)7O>#600De9$%>2LVd81Yeb1-X-P(Y5yQ%LXFPyHJS9LOm(=3qqRfJl%=|nCVNOM5 zpg0#u+&RCXvM4h>ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=Cn>wTzx1(qV@bS z0hYvspf(--lM>otrqbK$7p{3DzJ|+KN8%5ows)AI?zWk_n>jwEHXrTJecpEW_0xL= z?}N`*R`T~d2{AN${y8T#GEn4hUb&52^}Op@TW4{oc)A6)%$5=G}h# z?O{QLj@aRcAIf&y&OiUN=H2gq=_}V|pWfuReDV|{jwXw~>#w)I|9${XE z)7O>#600Dep5bGK9wD%hYeb1-X-P(Y5yQ%LXFPyHJS9LOm(=3qqRfJl%=|nCVNOM5 zpg0#u+&RCXvM4h>ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=Cn>w>~AWNX^a2R zbkveVY|45D7UnZ&JtjPwvdCCscZp0EA*0()#GOw)UH4-^&)y^E*4%UC)*|J}q_Ss;tN`nd8$>x9$_Xb^O2EpX&@C ZI46EzbLxq-voTO7gQu&X%Q~loCIF_C`w;*D diff --git a/docs/static/images/icons/callouts/14.png b/docs/static/images/icons/callouts/14.png deleted file mode 100644 index 64014b75fe2e84d45ed861974c72462727979360..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 411 zcmV;M0c8G(P)!ax*-PXaQ9e~6^e1gu=a6a&KSz}bR`+prYG9ayB$BDjWGfIE;t#wl!+ zR3S(jA%y#i_@eOOedXoc%RQe%L;wH~k+s%ZI~)!<=dD%?4MaplaU9QPGski2q3`>r z(}{j@0a$CLl+)={2vLWml*i-oa5#J}DW$gCZB~Z!(!M#)2St|1_V^0qpmCrBof=Y&NUas@LmfSw=)4B4f;8Fu)(eFsv24 zJzXxBrayquXcR?J{XE z)7O>#600De0j~t#c`vY#Yeb1-X-P(Y5yQ%LXFPyHJS9LOm(=3qqRfJl%=|nCVNOM5 zpg0#u+&RCXvM4h>ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=9Eq_Jl&Ka(%QdX zh{H8O%#_7)Tc@t$mM`p4(Ne7omR*~(>gd8_8AZH{=3ms$Fmzm^yL@_+(#aQQ5>7QW z>3g2fIsH(ugM)!V$x4Rr_+!J_XU%4xbz0aE;^N{m@42Z|@0S@TQ=WbP`TMV5Ok;<| z^Ihv+@6tQ{sciRF9dD7Nr=KobwJJ68zJK$<1Pd9rz%4O)*;}Jzj&~nTGMecz>B%lV zK|`fmIc8mp-h8iSXiGFW=C(L+XH4DRxZQX87^-dLuD>odo6YLT@Sw)dfBEIG)v2@6 zR)%mL7GRj1x-&v&+2q@A%a&h0`Lw7|#(w_!tgT!PoJ|+re`lxaY7e*=hH)_rZeB4|imU1$R#1`!P>&$poQl;nzm}mD5ZFopaX|GsS%q*{P~< z;WtmO%lhToBL0i}yfkaOt?EN=nkLNGuU`ywhI5H)L`iUdT1k0gQ7VIjhO(w-Zen_> zZ(@38a<+nro{^q~f~BRtfrY+-p+a&|W^qZSLvCepNoKNMYO!8QX+eHoiC%Jk?!;Y+ zJAlS%fsM;d&r2*R1)67JkeZlkYGj#gX_9E3W@4U_nw*@Ln38B@k(iuhnUeN2eF0kK0(Y1u|9Rc(19XFPiEBhjaDG}zd16s2gM)^$re|(qda7?? zdS-IAf{C7yo`r&?rM`iMzJZ}aa#3b+Nu@(>WpPPnvR-PjUP@^}eqM=Qa(?c_U5Yz^ z#%Y0#%S_KpEGY$=XJL?(l#*ybuErX#^g`ttQfwn3r>K)tuC)r#2`iJ>Prt42#Ndx#Uc~1)>aw z3jE@Q4|!9Z%lVv}- zc=48cF7H)t`(Ck`^+mtha~Np7bBSw2NpOBzNqJ&XDuaWDvZiNlVtT4?VtQtBwt|VC zk)DNurKP@sg}#BILUK`NaY>~^Ze?*vX0l#tv0h4PL4IC|UUGi!#9fLzfW~Qojmu2W zODrh`nrE42VU(7fm~5G9U~HM3l#*m_WNcxOXkuzEX4g z+-vfUhb0A>b04=Im{6XiQd1v%r%>h0$G8U7E1If8OQ!N~xOYY5h0NDT$p9(iZ?Q&e z18-(+l~J8O`)kc}e&uL$eW&>P-#`~Qm$*ih1m~xflqVLYGB{``YkKA;rl!p+yCFkc(+@-h!Xq*<< zxXkpt#FA2=d1VEBsYynrsitN|Y01eJ$;p;U#>wWX2KP5v&I9V=1L+C? fTFYQ)RAFeOZJ=$?lDoSWD8u0C>gTe~DWM4f^}upZ diff --git a/docs/static/images/icons/callouts/6.png b/docs/static/images/icons/callouts/6.png deleted file mode 100644 index 0ba694af6c07d947d219b45a629bd32c60a0f5fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQ*)Bra@SU# zmiz#bR~{$s2si{S(aY|Z}Vd7tb ouUmn-_&~Y>fYve?8dVq?X&Y!8wB+ut1u%w%U~xZhnMEEs6JbBSw2NpOBzNqJ&XDuaWDvZiNlVtT4?VtQtBwt|VC zk)DNurKP@sg}#BILUK`NaY>~^Ze?*vX0l#tv0h4PL4IC|UUGi!#9fLzfW~Qojmu2W zODrh`nrCEbVQgk$XkwI@Y+{_8nv`N>YGIaQkz#0QY@Te9lBQ<)awbq0A4pdK&{_sV bqY6VKZ3AtCmfYR7Kp6&4S3j3^P6u&S`V$cAh@R~F=4@V4jxkzlaQrcFYWK{)(`o5XZnut z=nE4SU2g1ZW%;@@I$>_e3F8a=8WK~|CVXt1DqisQxtIX|`YW_n&?Nh#1gQ}d)$LrYTw(_{nVG)tp2V+#}WG*e^KRLdkoLz7g? qn(IA84Qgo42`r6v<+Hvch>@C7(8A5T-G@yGywn*$#_oy diff --git a/docs/static/images/icons/callouts/9.png b/docs/static/images/icons/callouts/9.png deleted file mode 100644 index a0676d26cc2ff1de12c4ecdeefb44a0d71bc6bde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQNRqa;^5&H%t0&v*|C|wdb9$wI zR@+N9#RIowg@Uqn&z-__Tzhhz!sG|vTxA7?=O|Y?u(d4T{!RM9c7chr6d%1?R=i16 z?@Ic{f32YJFJnVhX)qGzOMplv!L->5yAlT#}irms+fsQd*FoSE84k zpF44v;trs3T43Wc)AJHbN`dAXo0u6Hr<$gkq?lM38ycjV7+5A5Sr{ayr5c%-n;95g pF*H#D>f!_G3IJNmU}#ifXryhRZP1dtyA~+J;OXk;vd$@?2>@J{cB%jX diff --git a/docs/static/images/icons/caution.png b/docs/static/images/icons/caution.png deleted file mode 100644 index 9a8c515a109faf9159777ac9e6861c2b8e7df383..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2734 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 diff --git a/docs/static/images/icons/example.png b/docs/static/images/icons/example.png deleted file mode 100644 index 1199e864f8502a57c3b8def7c2e1d90c5f760a73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2599 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 JPDHLkV1oa#`7Hnd diff --git a/docs/static/images/icons/home.png b/docs/static/images/icons/home.png deleted file mode 100644 index 37a5231bacc905886c9351a291e0d5eed68dd60e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1340 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 M07*qoM6N<$g4<+Y4gdfE diff --git a/docs/static/images/icons/note.png b/docs/static/images/icons/note.png deleted file mode 100644 index 7c1f3e2fa7ef8897bb55ee0e0dda088dc49d6408..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2494 zcmV;v2|@OWP)Dn;hlg!sWaK|u z738H>q0O5&+vt7wuzAz%lqL_6pE$;`=_$TGUSZXTKE{?UTV#Q4eNlNy5f~X6vC;eP zVar?IL^}QqvwNN<9++V9)D&M@mNI(ZJ=}l){r{0jyrc*W4-ebj%iAbbjxqbUooH}4 zuZUNi7mlaxe8H{Yp@$wSHWB%^MdKw!V8@OfbY&U!zwaWH1ECZMSs=**Axr2VTE>nY zJBTp?U=3MN%Nye7^JNh+5usEnkslf-lmqG&b&3m$`XUlKILOS*6L|lEeL0JH(vmpIP@3Z^}f3}I(e8SSttw+1qk&H*S&}e3GMAA ze*25>uxL>y9qlEuEJammu}RJZ5CH{IL<&-v!LqCF#LVnM6r2|T?>)7XyLk5G&`*lKvIwdGy#_(nzjI3(5CqS2q6%0s89LCN8ZARpWMe?PmD9V?HShd%D^46n-da~Q0bB!|EBq>kbB>Eu-+5C=y+<^lE)vp=*IvDh z&wTt&rcO=q;MU#j89PizM@H}-Rga;VI;Lj~b&!c7sK&G4*;xL@AYv=42@Ky5Lkv(r``>09+Sn36l=6 z84v~GqWqsXfEXi~07CEyNgBxW6tgmEaLm=~L<2=p=SYIbsghVP_0%`Ra6A$gv2LUnu6t|!IS4XAcz)5gy2b%K$<2%qgy+N3n``vNcr^Q zmN%H6A>=IzEhvB(BZ#2Rkw+mU4vPUNDKQ4}7*SQ6I$|_XE%HVk!F!HZs!SX_&UNe8 zV*mzw_wDD{i7DQ&ZY_yR5m5{S?2-V?s}HCuqD8fE!Q(XHcTbKIP5Il?2N)Zlp}ni0 za%UfZ+;xO+eeXBy-2EKRC+t6XlI=Ujd1TvD?EA+dL=(RI{a^Cj{$q4^cJkf(9z?V- zy=Zh7FQM}W(AiN&)T2Jd`+%zF=0aLf|5ja=Oj3vEYJhk@_ zcieg%Z{4_#y<_|N!;^pK%{N|)i16Nb-pC`5Zs*$9E@RE=6|KR{@N{=|@#U}mkozBa zj9YHH9ytHdc0QR`4!oEwUD8c=cYEu3QFjMp&rY!6`ZX<(dEW4(%~6}32H?SmA7lON zR&w3Ct9g3QKbfn~0`2E6ziF8+3;xuMO6%lbHX&cK1 zdW%z5qc3DxiJNa&2Ph%Hrs124#YGz`7*fm{3t91PH2%;Sh14O&014#tFeAXyE|BL>6|K8yg#=x3{Y}v$mQ|(ef=8&(R{e zL9_a)N)Z7RXqL$0RL7Y$UOcE4&XC3=YFzLj)09gtqt8D33|9;d6d^{yIQ_RMW=f2X zJSurEg~sANv%)wU9tJ56E=`rjI*mptS^!FtWMKeo-MZDbZTmI1-F7=51=pLSuF;4{ zF{zqqL#5?pYt9WczAK7ZY-vG5hcpI7K^jBj#pRMHG%+#32i|iVi98n4YW;xYr_3{riW@h)^doN%7{9V+h zk6;!#cyN-uUgNL3_Aq(mIDg$ehQ&xndmCL{ZLD10&oygSAp(N~y+srn0E~zbkyt-# zpQs+SIn0db4jg3nt}!Z=%7OqWPF0rk)D zr>8l2s>1lB@$ff)#?*sGJVnVz0zW@d(3twxNo#p`CGHJ_WD_uk{2YrQse z#LSqPnK|p?*UYHb>#ak)bB-iQNRot7sf73b^6y4fwdLW$YlU&m+vcV=Lw@G-xp$l1 z`?IJaBE%S5qeD}ZB)L2a{V&M>_pg-y4e|OV diff --git a/docs/static/images/icons/prev.png b/docs/static/images/icons/prev.png deleted file mode 100644 index 3e8f12fe24c6e042bc7ef4badb15449f71529ace..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1348 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 AfdBvi diff --git a/docs/static/images/janusgraph-logo.png b/docs/static/images/janusgraph-logo.png deleted file mode 100644 index bc77e5acd85b0beac76d6283fc0a2886b43f5fd9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4199 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 YGBq^$9{?b^!S?~cf#8C#vGz~?FRf;exBvhE diff --git a/docs/static/images/janusgraph-logomark.png b/docs/static/images/janusgraph-logomark.png deleted file mode 100644 index 72451c0dfbd6afb1fd3d94394067daba77b137bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1944 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

76jD)PVoP&Ib!ZF2CrMr8kRB)<_ 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

#x7=t#wbM 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 z%0D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 z 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 r@De-a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(l.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:k&&!k.call("\ufeff\xa0")?function(a){return null==a?"":k.call(a)}:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||n.guid++,e):void 0},now:function(){return+new Date},support:l}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="

",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="
","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=a.document,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,B=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:A.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:z,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=z.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return y.find(a);this.length=1,this[0]=d}return this.context=z,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};B.prototype=n.fn,y=n(z);var C=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!n(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function E(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return E(a,"nextSibling")},prev:function(a){return E(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(D[a]||(e=n.unique(e)),C.test(a)&&(e=e.reverse())),this.pushStack(e)}});var F=/\S+/g,G={};function H(a){var b=G[a]={};return n.each(a.match(F)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?G[a]||H(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&n.each(arguments,function(a,c){var d;while((d=n.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){if(a===!0?!--n.readyWait:!n.isReady){if(!z.body)return setTimeout(n.ready);n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(z,[n]),n.fn.trigger&&n(z).trigger("ready").off("ready"))}}});function J(){z.addEventListener?(z.removeEventListener("DOMContentLoaded",K,!1),a.removeEventListener("load",K,!1)):(z.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(z.addEventListener||"load"===event.type||"complete"===z.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===z.readyState)setTimeout(n.ready);else if(z.addEventListener)z.addEventListener("DOMContentLoaded",K,!1),a.addEventListener("load",K,!1);else{z.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&z.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!n.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}J(),n.ready()}}()}return I.promise(b)};var L="undefined",M;for(M in n(l))break;l.ownLast="0"!==M,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c=z.getElementsByTagName("body")[0];c&&(a=z.createElement("div"),a.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",b=z.createElement("div"),c.appendChild(a).appendChild(b),typeof b.style.zoom!==L&&(b.style.cssText="border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1",(l.inlineBlockNeedsLayout=3===b.offsetWidth)&&(c.style.zoom=1)),c.removeChild(a),a=b=null)}),function(){var a=z.createElement("div");if(null==l.deleteExpando){l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}}a=null}(),n.acceptData=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(n.acceptData(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f -}}function S(a,b,c){if(n.acceptData(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d]));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},X=/^(?:checkbox|radio)$/i;!function(){var a=z.createDocumentFragment(),b=z.createElement("div"),c=z.createElement("input");if(b.setAttribute("className","t"),b.innerHTML="
a",l.leadingWhitespace=3===b.firstChild.nodeType,l.tbody=!b.getElementsByTagName("tbody").length,l.htmlSerialize=!!b.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==z.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,a.appendChild(c),l.appendChecked=c.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,a.appendChild(b),b.innerHTML="",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){l.noCloneEvent=!1}),b.cloneNode(!0).click()),null==l.deleteExpando){l.deleteExpando=!0;try{delete b.test}catch(d){l.deleteExpando=!1}}a=b=c=null}(),function(){var b,c,d=z.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),l[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var Y=/^(?:input|select|textarea)$/i,Z=/^key/,$=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,ab=/^([^.]*)(?:\.(.+)|)$/;function bb(){return!0}function cb(){return!1}function db(){try{return z.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof n===L||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(F)||[""],h=b.length;while(h--)f=ab.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(F)||[""],j=b.length;while(j--)if(h=ab.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,m,o=[d||z],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||z,3!==d.nodeType&&8!==d.nodeType&&!_.test(p+n.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[n.expando]?b:new n.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),k=n.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!n.isWindow(d)){for(i=k.delegateType||p,_.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||z)&&o.push(l.defaultView||l.parentWindow||a)}m=0;while((h=o[m++])&&!b.isPropagationStopped())b.type=m>1?i:k.bindType||p,f=(n._data(h,"events")||{})[b.type]&&n._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&n.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&n.acceptData(d)&&g&&d[p]&&!n.isWindow(d)){l=d[g],l&&(d[g]=null),n.event.triggered=p;try{d[p]()}catch(r){}n.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((n.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?n(c,this).index(i)>=0:n.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ib=/^\s+/,jb=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,kb=/<([\w:]+)/,lb=/\s*$/g,sb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]},tb=eb(z),ub=tb.appendChild(z.createElement("div"));sb.optgroup=sb.option,sb.tbody=sb.tfoot=sb.colgroup=sb.caption=sb.thead,sb.th=sb.td;function vb(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==L?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==L?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,vb(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function wb(a){X.test(a.type)&&(a.defaultChecked=a.checked)}function xb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function yb(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function zb(a){var b=qb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ab(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}function Bb(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Cb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(yb(b).text=a.text,zb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&X.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}n.extend({clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!hb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ub.innerHTML=a.outerHTML,ub.removeChild(f=ub.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=vb(f),h=vb(a),g=0;null!=(e=h[g]);++g)d[g]&&Cb(e,d[g]);if(b)if(c)for(h=h||vb(a),d=d||vb(f),g=0;null!=(e=h[g]);g++)Bb(e,d[g]);else Bb(a,f);return d=vb(f,"script"),d.length>0&&Ab(d,!i&&vb(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k,m=a.length,o=eb(b),p=[],q=0;m>q;q++)if(f=a[q],f||0===f)if("object"===n.type(f))n.merge(p,f.nodeType?[f]:f);else if(mb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(kb.exec(f)||["",""])[1].toLowerCase(),k=sb[i]||sb._default,h.innerHTML=k[1]+f.replace(jb,"<$1>")+k[2],e=k[0];while(e--)h=h.lastChild;if(!l.leadingWhitespace&&ib.test(f)&&p.push(b.createTextNode(ib.exec(f)[0])),!l.tbody){f="table"!==i||lb.test(f)?""!==k[1]||lb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)n.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}n.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),l.appendChecked||n.grep(vb(p,"input"),wb),q=0;while(f=p[q++])if((!d||-1===n.inArray(f,d))&&(g=n.contains(f.ownerDocument,f),h=vb(o.appendChild(f),"script"),g&&Ab(h),c)){e=0;while(f=h[e++])pb.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.deleteExpando,m=n.event.special;null!=(d=a[h]);h++)if((b||n.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k?delete d[i]:typeof d.removeAttribute!==L?d.removeAttribute(i):d[i]=null,c.push(f))}}}),n.fn.extend({text:function(a){return W(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||z).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(vb(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&Ab(vb(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(vb(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return W(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(gb,""):void 0;if(!("string"!=typeof a||nb.test(a)||!l.htmlSerialize&&hb.test(a)||!l.leadingWhitespace&&ib.test(a)||sb[(kb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(jb,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(vb(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(vb(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,o=k-1,p=a[0],q=n.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&ob.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(i=n.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=n.map(vb(i,"script"),yb),f=g.length;k>j;j++)d=i,j!==o&&(d=n.clone(d,!0,!0),f&&n.merge(g,vb(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,n.map(g,zb),j=0;f>j;j++)d=g[j],pb.test(d.type||"")&&!n._data(d,"globalEval")&&n.contains(h,d)&&(d.src?n._evalUrl&&n._evalUrl(d.src):n.globalEval((d.text||d.textContent||d.innerHTML||"").replace(rb,"")));i=c=null}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],g=n(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Db,Eb={};function Fb(b,c){var d=n(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:n.css(d[0],"display");return d.detach(),e}function Gb(a){var b=z,c=Eb[a];return c||(c=Fb(a,b),"none"!==c&&c||(Db=(Db||n(" - - - - - diff --git a/janusgraph-dist/src/release/release.sh b/janusgraph-dist/src/release/release.sh index 2d002434be..ce4811a48a 100755 --- a/janusgraph-dist/src/release/release.sh +++ b/janusgraph-dist/src/release/release.sh @@ -94,7 +94,3 @@ set -x # Deploy artifacts to S3 and to Sonatype Staging. Doesn't push to Github. $git_cmd $mvn_cmd - -# Stage gh-pages updates locally. This commits new gh-pages content in the -# local clone, but it doesn't push to Github. -janusgraph-dist/target/release/gh-pages-update.sh release.properties diff --git a/janusgraph-doc/pom.xml b/janusgraph-doc/pom.xml index 5147d303da..cc71864c11 100644 --- a/janusgraph-doc/pom.xml +++ b/janusgraph-doc/pom.xml @@ -8,20 +8,12 @@ pomjanusgraph-doc - JanusGraph-Doc: AsciiDoc Manual for JanusGraph + JanusGraph-Doc: Manual for JanusGraphhttp://janusgraph.org ${project.basedir}/.. - - - ${project.parent.basedir}/docs - ${project.parent.basedir}/docs/xsl - ${project.basedir}/target/docs - ${asciidoc.output.dir}/xsl - ${project.basedir}/target/docs/docbook - ${project.basedir}/target/docs/single - ${project.basedir}/target/docs/chunk + ${project.parent.basedir}/docs @@ -63,59 +55,6 @@ default-testResources none - - filter-doc-sources-and-xsl - generate-sources - - copy-resources - - - ${asciidoc.output.dir} - - $MAVEN{*} - - - - ${asciidoc.input.dir} - true - - - - - - copy-static-assets-to-htmlsingle - process-resources - - copy-resources - - false - - ${htmlsingle.output.dir} - - - ${asciidoc.input.dir}/static - false - - - - - - copy-static-assets-to-htmlchunk - process-resources - - copy-resources - - false - - ${htmlchunk.output.dir} - - - ${asciidoc.input.dir}/static - false - - - - @@ -134,62 +73,13 @@ org.janusgraph.util.system.ConfigurationPrinter org.janusgraph.graphdb.configuration.GraphDatabaseConfiguration.ROOT_NS - ${asciidoc.output.dir}/listings/janusgraph_cfg.adoc - true + ${doc.dir}/basics/janusgraph-cfg.md - - org.asciidoctor - asciidoctor-maven-plugin - false - - - asciidoc-to-docbook - generate-resources - - process-asciidoc - - - ${asciidoc.output.dir} - index.adoc - ${docbook.output.dir} - docbook5 - book - - UTF-8 - images/icons - images - true - - - - - - - - maven-install-plugin - - - default-install - none - - - - - - maven-source-plugin - - - attach-sources - none - - - - maven-javadoc-plugin @@ -199,100 +89,6 @@ - - - maven-deploy-plugin - - - default-deploy - none - - - - - - org.codehaus.mojo - xml-maven-plugin - false - - - docbook-to-htmlsingle - process-resources - - transform - - false - - true - - - ${docbook.output.dir} - ${htmlsingle.output.dir} - - index.xml - - ${xsl.output.dir}/single.xsl - - - .html - - - - - - - - docbook-to-htmlchunk - process-resources - - transform - - false - - true - - - ${docbook.output.dir} - - index.xml - - ${xsl.output.dir}/chunk.xsl - - - .html - - - - - - - - - - net.sf.xslthl - xslthl - 2.1.0 - - - saxon - saxon - 6.5.3 - - - - - - maven-compiler-plugin - - - compile-tests - test-compile - - testCompile - - - - diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000..8e32fcf009 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,109 @@ +site_name: JanusGraph + +repo_name: 'JanusGraph' +repo_url: 'https://github.com/janusgraph/janusgraph' + +copyright: 'Copyright © 2019 JanusGraph Authors. All rights reserved.
The Linux Foundation has registered trademarks and uses trademarks. For a list of
trademarks of The Linux Foundation, please see our Trademark Usage page.
Cassandra, Groovy, HBase, Hadoop, Lucene, Solr, and TinkerPop are trademarks of the Apache Software Foundation.
Berkeley DB and Berkeley DB Java Edition are trademarks of Oracle.' + +plugins: + - search + - markdownextradata + +theme: + language: 'en' + name: 'material' + feature: + tabs: false + palette: + primary: 'green' + accent: 'teal' + font: false + logo: 'janusgraph-logomark.svg' + favicon: 'favicon.ico' + +extra_css: + - 'theme/structor-menu.css' + - 'theme/extra.css' + +extra: + social: + - type: github-alt + link: https://github.com/janusgraph + - type: twitter + link: https://twitter.com/janusgraph + - type: envelope + link: https://groups.google.com/group/janusgraph-users + - type: envelope + link: https://groups.google.com/group/janusgraph-dev + +markdown_extensions: + - pymdownx.superfences + - pymdownx.snippets + - markdown.extensions.admonition + - markdown_include.include: + base_path: docs + - markdown.extensions.codehilite: + linenums: True + +nav: + - Introduction: index.md + - JanusGraph Basics: + - Configuration: basics/configuration.md + - Schema and Data Modeling: basics/schema.md + - Gremlin Query Language: basics/gremlin.md + - JanusGraph Server: basics/server.md + - Deployment Scenarios: basics/deployment.md + - ConfiguredGraphFactory: basics/configured-graph-factory.md + - Things to Consider in a Multi-Node JanusGraph Cluster: basics/multi-node.md + - Indexing for Better Performance: basics/index-performance.md + - Transactions: basics/transactions.md + - JanusGraph Cache: basics/cache.md + - Transaction Log: basics/transaction-log.md + - Configuration Reference: basics/configuration-reference.md + - Config Example: basics/example-config.md + - Common Questions: basics/common-questions.md + - Technical Limitations: basics/technical-limitations.md + - Storage Backends: + - Introduction: storage-backend/index.md + - Apache Cassandra: storage-backend/cassandra.md + - Apache HBase: storage-backend/hbase.md + - Google Cloud Bigtable: storage-backend/bigtable.md + - Oracle Berkeley DB Java Edition: storage-backend/bdb.md + - InMemory Storage Backend: storage-backend/inmemorybackend.md + - Index Backends: + - Introduction: index-backend/index.md + - Search Predicates and Data Types: index-backend/search-predicates.md + - Index Parameters and Full-Text Search: index-backend/text-search.md + - Field Mapping: index-backend/field-mapping.md + - Direct Index Query: index-backend/direct-index-query.md + - Elasticsearch: index-backend/elasticsearch.md + - Apache Solr: index-backend/solr.md + - Apache Lucene: index-backend/lucene.md + - Advanced Topics: + - Advanced Schema: advanced-topics/advschema.md + - Eventually-Consistent Storage Backends: advanced-topics/eventual-consistency.md + - Failure & Recovery: advanced-topics/recovery.md + - Index Management: advanced-topics/index-admin.md + - Bulk Loading: advanced-topics/bulk-loading.md + - Graph Partitioning: advanced-topics/partitioning.md + - Datatype and Attribute Serializer Configuration: advanced-topics/serializer.md + - JanusGraph with TinkerPop’s Hadoop-Gremlin: advanced-topics/hadoop.md + - Monitoring JanusGraph: advanced-topics/monitoring.md + - Migrating from Titan: advanced-topics/migrating.md + - JanusGraph Data model: advanced-topics/data-model.md + - JanusGraph Bus: advanced-topics/janusgraph-bus.md + - Connecting to JanusGraph: + - Introduction: connecting/index.md + - Using Java: connecting/java.md + - Using Python: connecting/python.md + - Using .Net: connecting/dotnet.md + - Development: development.md + - Appendices: appendices.md + - Changelog: changelog.md + +extra: + latest_version: 0.2.3 + snapshot_version: 0.2.3-SNAPSHOT + tinkerpop_version: 3.2.9 + hadoop2_version: 2.7.2 + jamm_version: 0.3.0 diff --git a/pom.xml b/pom.xml index b2450c7f8c..069543dde8 100644 --- a/pom.xml +++ b/pom.xml @@ -444,17 +444,6 @@ stanza that matches the github tag below. Here's an example settings.xml: - - - ... - - ... - - github << must match dalaro - allmypasswordsareliterallypassword - - - --> github janusgraph @@ -465,11 +454,6 @@ maven-antrun-plugin 1.7 - - org.asciidoctor - asciidoctor-maven-plugin - 0.1.4 - org.jacoco jacoco-maven-plugin @@ -1509,184 +1493,5 @@ - - - asciidoc - - false - - asciidoc - - - - - ${project.basedir}/docs - ${asciidoc.input.dir}/xsl - ${project.basedir}/target/docs/xsl - ${project.basedir}/target/docs/docbook - ${project.basedir}/target/docs/htmlsingle - ${project.basedir}/target/docs/htmlchunk - - - - - - org.asciidoctor - asciidoctor-maven-plugin - - - asciidoc-to-docbook - generate-resources - - process-asciidoc - - false - - ${asciidoc.input.dir} - index.adoc - ${docbook.output.dir} - docbook5 - book - - images - UTF-8 - true - - - - - - - org.codehaus.mojo - xml-maven-plugin - - - docbook-to-htmlsingle - process-resources - - transform - - false - - true - - - ${docbook.output.dir} - ${htmlsingle.output.dir} - - index.xml - - ${xsl.output.dir}/single.xsl - - - .html - - - - - - - - docbook-to-htmlchunk - process-resources - - transform - - false - - true - - - ${docbook.output.dir} - - index.xml - - ${xsl.output.dir}/chunk.xsl - - - .html - - - - - - - - - - net.sf.xslthl - xslthl - 2.1.0 - - - saxon - saxon - 6.5.3 - - - - - maven-resources-plugin - - - filter-xsl-stylesheets - generate-sources - - copy-resources - - false - - ${xsl.output.dir} - - $MAVEN{*} - - - - ${xsl.input.dir} - true - - - - - - copy-static-assets-to-htmlsingle - process-resources - - copy-resources - - false - - ${htmlsingle.output.dir} - - - ${asciidoc.input.dir}/static - false - - - - - - copy-static-assets-to-htmlchunk - process-resources - - copy-resources - - false - - ${htmlchunk.output.dir} - - - ${asciidoc.input.dir}/static - false - - - - - - - - - - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..42a360df01 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +mkdocs==1.0.4 +mkdocs-material==4.2.0 +Pygments==2.3.1 +pymdown-extensions==6.0 +mkdocs-markdownextradata-plugin==0.0.5 +markdown-include==0.5.1 \ No newline at end of file From c6510b20608889543f8844b28e4786d8fe86ddc3 Mon Sep 17 00:00:00 2001 From: Jan Jansen Date: Wed, 21 Aug 2019 12:52:14 +0200 Subject: [PATCH 2/3] Add remarks for added features [doc only] Signed-off-by: Jan Jansen --- docs/advanced-topics/serializer.md | 3 +++ docs/index-backend/field-mapping.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/advanced-topics/serializer.md b/docs/advanced-topics/serializer.md index 1ebfd67947..c8188fbaef 100644 --- a/docs/advanced-topics/serializer.md +++ b/docs/advanced-topics/serializer.md @@ -1,6 +1,9 @@ Datatype and Attribute Serializer Configuration =============================================== +!!! Info + `ObjectNode` and `ArrayNode` serializers are added in JanusGraph version `0.3.2` + JanusGraph supports a number of classes for attribute values on properties. JanusGraph efficiently serializes primitives, primitive arrays and `Geoshape`, `UUID`, `Date`, `ObjectNode` and `ArrayNode`. diff --git a/docs/index-backend/field-mapping.md b/docs/index-backend/field-mapping.md index 60361a7ab1..8387ed3908 100644 --- a/docs/index-backend/field-mapping.md +++ b/docs/index-backend/field-mapping.md @@ -105,6 +105,9 @@ With these settings, JanusGraph will use a SimpleAnalyzer analyzer for property ### Custom parameters +!!! Info + Custom parameters are added in JanusGraph version `0.3.1` + Sometimes it is required to set additional parameters on mappings (other than mapping type, mapping name and analyzer). For example, when we would like to use a different similarity algorithm (to modify the scoring algorithm of full text search) or if we want to use a custom boosting on some fields in Elasticsearch we can set custom parameters (right now only Elasticsearch supports custom parameters). The name of the custom parameter must be set through `ParameterType.customParameterName("yourProperty")`. From b87ba538d42351560ad9a7e47ed7f75611a799b7 Mon Sep 17 00:00:00 2001 From: Jan Jansen Date: Thu, 22 Aug 2019 22:24:08 +0200 Subject: [PATCH 3/3] Changes for branch 0.4 Signed-off-by: Jan Jansen --- docs/basics/index-performance.md | 18 +++++----- docs/basics/janusgraph-cfg.md | 2 ++ docs/changelog.md | 58 +++++++++++++++++++++++++++++++ docs/index-backend/text-search.md | 13 +++++++ docs/storage-backend/cassandra.md | 4 +++ mkdocs.yml | 10 +++--- 6 files changed, 91 insertions(+), 14 deletions(-) diff --git a/docs/basics/index-performance.md b/docs/basics/index-performance.md index 17d40cfd16..84ec12f9d1 100644 --- a/docs/basics/index-performance.md +++ b/docs/basics/index-performance.md @@ -269,10 +269,10 @@ expects two parameters: results will be ordered by the value of the vertices or edges for this property key. -- The sort order: either increasing `incr` or decreasing `decr` +- The sort order: either ascending `asc` or descending `desc` For example, the query -`g.V().has('name', textContains('hercules')).order().by('age', decr).limit(10)` +`g.V().has('name', textContains('hercules')).order().by('age', desc).limit(10)` retrieves the ten oldest individuals with *hercules* in their name. When using `order().by()` it is important to note that: @@ -362,7 +362,7 @@ graph.tx().rollback() //Never create new indexes while a transaction is active mgmt = graph.openManagement() time = mgmt.getPropertyKey('time') battled = mgmt.getEdgeLabel('battled') -mgmt.buildEdgeIndex(battled, 'battlesByTime', Direction.BOTH, Order.decr, time) +mgmt.buildEdgeIndex(battled, 'battlesByTime', Direction.BOTH, Order.desc, time) mgmt.commit() //Wait for the index to become available ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByTime').call() @@ -373,7 +373,7 @@ mgmt.commit() ``` This example builds a vertex-centric index which indexes `battled` edges -in both direction by time in decreasing order. A vertex-centric index is +in both direction by time in descending order. A vertex-centric index is built against a particular edge label which is the first argument to the index construction method `JanusGraphManagement.buildEdgeIndex()`. The index only applies to edges of this label - `battled` in the example @@ -400,7 +400,7 @@ mgmt = graph.openManagement() time = mgmt.getPropertyKey('time') rating = mgmt.makePropertyKey('rating').dataType(Double.class).make() battled = mgmt.getEdgeLabel('battled') -mgmt.buildEdgeIndex(battled, 'battlesByRatingAndTime', Direction.OUT, Order.decr, rating, time) +mgmt.buildEdgeIndex(battled, 'battlesByRatingAndTime', Direction.OUT, Order.desc, rating, time) mgmt.commit() //Wait for the index to become available ManagementSystem.awaitRelationIndexStatus(graph, 'battlesByRatingAndTime', 'battled').call() @@ -412,7 +412,7 @@ mgmt.commit() This example extends the schema by a `rating` property on `battled` edges and builds a vertex-centric index which indexes `battled` edges in -the out-going direction by rating and time in decreasing order. Note, +the out-going direction by rating and time in descending order. Note, that the order in which the property keys are specified is important because vertex-centric indexes are prefix indexes. This means, that `battled` edges are indexed by `rating` *first* and `time` *second*. @@ -469,8 +469,8 @@ to be traversed. Use the `localLimit` command to retrieve a subset of the edges (in a given order) for EACH vertex that is traversed. ```groovy h = g..V().has('name', 'hercules').next() -g.V(h).local(outE('battled').order().by('time', decr).limit(10)).inV().values('name') -g.V(h).local(outE('battled').has('rating', 5.0).order().by('time', decr).limit(10)).values('place') +g.V(h).local(outE('battled').order().by('time', desc).limit(10)).inV().values('name') +g.V(h).local(outE('battled').has('rating', 5.0).order().by('time', desc).limit(10)).values('place') ``` The first query asks for the names of the 10 most recently battled @@ -481,7 +481,7 @@ the number of elements to be returned. Such queries can also be efficiently answered by vertex-centric indexes if the order key matches the key of the index and the requested order -(i.e. increasing or decreasing) is the same as the one defined for the +(i.e. ascending or descending) is the same as the one defined for the index. The `battlesByTime` index would be used to answer the first query and `battlesByRatingAndTime` applies to the second. Note, that the `battlesByRatingAndTime` index cannot be used to answer the first query diff --git a/docs/basics/janusgraph-cfg.md b/docs/basics/janusgraph-cfg.md index f8e2cc91e2..0b42ee3ac4 100644 --- a/docs/basics/janusgraph-cfg.md +++ b/docs/basics/janusgraph-cfg.md @@ -306,6 +306,7 @@ Configuration options for query processing | Name | Description | Datatype | Default Value | Mutability | | ---- | ---- | ---- | ---- | ---- | | query.batch | Whether traversal queries should be batched when executed against the storage backend. This can lead to significant performance improvement if there is a non-trivial latency to the backend. | Boolean | false | MASKABLE | +| query.batch-property-prefetch | Whether to do a batched pre-fetch of all properties on adjacent vertices against the storage backend prior to evaluating a has condition against those vertices. Because these vertex properties will be loaded into the transaction-level cache of recently-used vertices when the condition is evaluated this can lead to significant performance improvement if there are many edges to adjacent vertices and there is a non-trivial latency to the backend. | Boolean | false | MASKABLE | | query.fast-property | Whether to pre-fetch all properties on first singular vertex property access. This can eliminate backend calls on subsequentproperty access for the same vertex at the expense of retrieving all properties at once. This can be expensive for vertices with many properties | Boolean | true | MASKABLE | | query.force-index | Whether JanusGraph should throw an exception if a graph query cannot be answered using an index. Doing solimits the functionality of JanusGraph's graph queries but ensures that slow graph queries are avoided on large graphs. Recommended for production use of JanusGraph. | Boolean | false | MASKABLE | | query.ignore-unknown-index-key | Whether to ignore undefined types encountered in user-provided index queries | Boolean | false | MASKABLE | @@ -462,6 +463,7 @@ CQL storage backend options | storage.cql.replication-factor | The number of data replicas (including the original copy) that should be kept | Integer | 1 | GLOBAL_OFFLINE | | storage.cql.replication-strategy-class | The replication strategy to use for JanusGraph keyspace | String | SimpleStrategy | FIXED | | storage.cql.replication-strategy-options | Replication strategy options, e.g. factor or replicas per datacenter. This list is interpreted as a map. It must have an even number of elements in [key,val,key,val,...] form. A replication_factor set here takes precedence over one set with storage.cql.replication-factor | String[] | (no default value) | FIXED | +| storage.cql.use-external-locking | True to prevent JanusGraph from using its own locking mechanism. Setting this to true eliminates redundant checks when using an external locking mechanism outside of JanusGraph. Be aware that when use-external-locking is set to true, that failure to employ a locking algorithm which locks all columns that participate in a transaction upfront and unlocks them when the transaction ends, will result in a 'read uncommitted' transaction isolation level guarantee. If set to true without an appropriate external locking mechanism in place side effects such as dirty/non-repeatable/phantom reads should be expected. | Boolean | false | MASKABLE | | storage.cql.write-consistency-level | The consistency level of write operations against Cassandra | String | QUORUM | MASKABLE | ### storage.cql.ssl diff --git a/docs/changelog.md b/docs/changelog.md index a116b12d4c..7b9f8d5c1e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -22,9 +22,67 @@ use the latest versions of the software. | 0.1.z| 1| 1.2.z, 2.0.z, 2.1.z| 0.98.z, 1.0.z, 1.1.z, 1.2.z| 0.9.z, 1.0.0-preZ, 1.0.0| 1.5.z| 5.2.z| 3.2.z| 1.6.z| 2.10.z| | 0.2.z | 1 | 1.2.z, 2.0.z, 2.1.z, 2.2.z, 3.0.z, 3.11.z | 0.98.z, 1.0.z, 1.1.z, 1.2.z, 1.3.z | 0.9.z, 1.0.0-preZ, 1.0.0 | 1.5-1.7.z, 2.3-2.4.z, 5.y, 6.y | 5.2-5.5.z, 6.2-6.6.z, 7.y | 3.2.z | 1.6.z | 2.10.z | | 0.3.z | 2 | 1.2.z, 2.0.z, 2.1.z, 2.2.z, 3.0.z, 3.11.z | 1.0.z, 1.1.z, 1.2.z, 1.3.z, 1.4.z | 1.0.0, 1.1.0, 1.1.2, 1.2.0, 1.3.0, 1.4.0 | 1.5-1.7.z, 2.3-2.4.z, 5.y, 6.y | 5.2-5.5.z, 6.2-6.6.z, 7.y | 3.3.z | 2.2.z | 2.11.z | +| 0.4.z | 2 | 2.1.z, 2.2.z, 3.0.z, 3.11.z | 1.2.z, 1.3.z, 1.4.z, 2.1.z | 1.3.0, 1.4.0, 1.5.z, 1.6.z, 1.7.z, 1.8.z, 1.9.z, 1.10.z, 1.11.z | 5.y, 6.y | 7.y | 3.4.z | 2.2.z | 2.11.z | ## Release Notes +### Version 0.4.0 (Release Date: July 1, 2019) + +```xml tab='Maven' + + org.janusgraph + janusgraph-core + 0.4.0 + +``` + +```groovy tab='Gradle' +compile "org.janusgraph:janusgraph-core:0.4.0" +``` + +**Tested Compatibility:** + +- Apache Cassandra 2.2.10, 3.0.14, 3.11.0 +- Apache HBase 1.2.6, 1.3.1, 1.4.10, 2.1.5 +- Google Bigtable 1.3.0, 1.4.0, 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0, 1.10.0, 1.11.0 +- Oracle BerkeleyJE 7.5.11 +- Elasticsearch 5.6.14, 6.0.1, 6.6.0 +- Apache Lucene 7.0.0 +- Apache Solr 7.0.0 +- Apache TinkerPop 3.4.1 +- Java 1.8 + +For more information on features and bug fixes in 0.4.0, see the GitHub milestone: + +* https://github.com/JanusGraph/janusgraph/milestone/8?closed=1 + +#### Upgrade Instructions + +##### HBase: Upgrade from 1.2 to 2.1 +The version of HBase that is included in the distribution of JanusGraph was upgraded from 1.2.6 to 2.1.5. +HBase 2.x client is not fully backward compatible with HBase 1.x server. Users who operate their own HBase version 1.x cluster may need to upgrade their cluster to version 2.x. +Optionally users may build their own distribution of JanusGraph which includes HBase 1.x from source with the maven flags -Dhbase.profile -Phbase1. + +##### Cassandra: Upgrade from 2.1 to 2.2 +The version of Cassandra that is included in the distribution of JanusGraph was upgraded from 2.1.20 to 2.2.13. +Refer to [the upgrade documentation of Cassandra](https://github.com/apache/cassandra/blob/174cf761f7897443080b8a840b649b7eab17ae25/NEWS.txt#L787) +for detailed instructions to perform this upgrade. +Users who operate their own Cassandra cluster instead of using Cassandra distributed together with JanusGraph are not affected by this upgrade. +This also does not change the different versions of Cassandra that are supported by JanusGraph (see <> for a detailed list of the supported versions). + +##### BerkeleyDB : Upgrade from 7.4 to 7.5 +The BerkeleyDB version has been updated, and it contains changes to the file format stored on disk +(see [the BerkeleyDB changelog for reference](https://docs.oracle.com/cd/E17277_02/html/changelog.html)). +This file format change is forward compatible with previous versions of BerkeleyDB, so existing graph data stored with JanusGraph can be read in. +However, once the data has been read in with the newer version of BerkeleyDB, those files can no longer be read by the older version. +Users are encouraged to backup the BerkeleyDB storage directory before attempting to use it with the JanusGraph release. + +##### Solr: Compatible Lucene version changed from 5.0.0 to 7.0.0 in distributed config +The JanusGraph distribution contains a `solrconfig.xml` file that can be used to configure Solr. +The value `luceneMatchVersion` in this config that tells Solr to behave according to that Lucene version was changed from 5.0.0 to 7.0.0 as that is the default version currently used by JanusGraph. +Users should generally set this value to the version of their Solr installation. +If the config distributed by JanusGraph is used for an existing Solr installation that used a lower version before (like 5.0.0 from a previous versions of this file), it is highly recommended that a re-indexing is performed. + ### Version 0.3.2 (Release Date: June 16, 2019) ```xml tab='Maven' diff --git a/docs/index-backend/text-search.md b/docs/index-backend/text-search.md index 8b5946a684..b144ffe39b 100644 --- a/docs/index-backend/text-search.md +++ b/docs/index-backend/text-search.md @@ -130,6 +130,19 @@ mgmt.commit() Note that the data will be stored in the index twice, once for exact matching and once for fuzzy matching. +TinkerPop Text Predicates +------------------------ + +It is also possible to use the TinkerPop text predicates with JanusGraph, but these predicates do not make use of +indices which means that they require filtering in memory which can be very costly. + +```groovy +import static org.apache.tinkerpop.gremlin.process.traversal.TextP.*; +g.V().has('bookname', startingWith('uni')) +g.V().has('bookname', endingWith('corn')) +g.V().has('bookname', containing('nico')) +``` + Geo Mapping ----------- diff --git a/docs/storage-backend/cassandra.md b/docs/storage-backend/cassandra.md index 8a3e258314..3f425720b7 100644 --- a/docs/storage-backend/cassandra.md +++ b/docs/storage-backend/cassandra.md @@ -35,6 +35,10 @@ deprecation of Thrift, and it has several classes that support Thrift. With Cassandra 4.0, Thrift support will be removed in Cassandra. JanusGraph users are recommended to use the `cql` storage backend. +!!! warning + Starting with JanusGraph 0.4.1, all non CQL-backends are deprecated, + including `cassandrathrift`, `cassandra` and `embeddedcassandra`. + !!! note If you plan to use a Thrift-based driver and you are using Cassandra 2.2 or higher, you need to explicitly enable Thrift so that JanusGraph diff --git a/mkdocs.yml b/mkdocs.yml index 0c8213110a..e60175981c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -117,8 +117,8 @@ nav: - Changelog: changelog.md extra: - latest_version: 0.3.2 - snapshot_version: 0.3.3-SNAPSHOT - tinkerpop_version: 3.3.3 - hadoop2_version: 2.7.2 - jamm_version: 0.3.0 + latest_version: 0.4.0 + snapshot_version: 0.4.1-SNAPSHOT + tinkerpop_version: 3.4.1 + hadoop2_version: 2.7.7 + jamm_version: 0.3.0 \ No newline at end of file