diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index ecdccbd7ffd..215c94426a3 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -137,6 +137,7 @@
  • Lens
  • Livy
  • Markdown
  • +
  • Neo4j
  • Pig
  • Postgresql, HAWQ
  • R
  • diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-config.png b/docs/assets/themes/zeppelin/img/docs-img/neo4j-config.png new file mode 100644 index 00000000000..2de3699e8a5 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-config.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png b/docs/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png new file mode 100644 index 00000000000..177e0a5e764 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-graph.png b/docs/assets/themes/zeppelin/img/docs-img/neo4j-graph.png new file mode 100644 index 00000000000..396b960db6c Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-graph.png differ diff --git a/docs/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif b/docs/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif new file mode 100644 index 00000000000..28c191516fd Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif differ diff --git a/docs/index.md b/docs/index.md index 5e991f1903f..dbec0402d97 100644 --- a/docs/index.md +++ b/docs/index.md @@ -142,6 +142,7 @@ limitations under the License. * [Lens](./interpreter/lens.html) * [Livy](./interpreter/livy.html) * [markdown](./interpreter/markdown.html) + * [Neo4j](./interpreter/neo4j.html) * [Pig](./interpreter/pig.html) * [Postgresql, HAWQ](./interpreter/postgresql.html) * [Python](./interpreter/python.html) diff --git a/docs/interpreter/neo4j.md b/docs/interpreter/neo4j.md new file mode 100644 index 00000000000..37f1f8c935d --- /dev/null +++ b/docs/interpreter/neo4j.md @@ -0,0 +1,117 @@ +--- +layout: page +title: "Neo4j Interpreter for Apache Zeppelin" +description: "Neo4j is a native graph database, designed to store and process graphs from bottom to top." +group: interpreter +--- + +{% include JB/setup %} + +# Neo4j Interpreter for Apache Zeppelin + +
    + +## Overview +[Neo4j](https://neo4j.com/product/) is a native graph database, designed to store and process graphs from bottom to top. + + +![Neo4j - Interpreter - Video]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-interpreter-video.gif) + +## Configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyDefaultDescription
    neo4j.urlbolt://localhost:7687The Neo4j's BOLT url.
    neo4j.auth.typeBASICThe Neo4j's authentication type (NONE, BASIC).
    neo4j.auth.userneo4jThe Neo4j user name.
    neo4j.auth.passwordneo4jThe Neo4j user password.
    neo4j.max.concurrency50Max concurrency call from Zeppelin to Neo4j server.
    + +
    + ![Interpreter configuration]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-config.png) +
    + + +## Enabling the Neo4j Interpreter +In a notebook, to enable the **Neo4j** interpreter, click the **Gear** icon and select **Neo4j**. + +## Using the Neo4j Interpreter +In a paragraph, use `%neo4j` to select the Neo4j interpreter and then input the Cypher commands. +For list of Cypher commands please refer to the official [Cyper Refcard](http://neo4j.com/docs/cypher-refcard/current/) + +```bash +%neo4j +//Sample the TrumpWorld dataset +WITH +'https://docs.google.com/spreadsheets/u/1/d/1Z5Vo5pbvxKJ5XpfALZXvCzW26Cl4we3OaN73K9Ae5Ss/export?format=csv&gid=1996904412' AS url +LOAD CSV WITH HEADERS FROM url AS row +RETURN row.`Entity A`, row.`Entity A Type`, row.`Entity B`, row.`Entity B Type`, row.Connection, row.`Source(s)` +LIMIT 10 +``` + +The Neo4j interpreter leverages the [Network display system](../usage/display_system/basic.html#network) allowing to visualize the them directly from the paragraph. + + +### Write your Cypher queries and navigate your graph + +This query: + +```bash +%neo4j +MATCH (vp:Person {name:"VLADIMIR PUTIN"}), (dt:Person {name:"DONALD J. TRUMP"}) +MATCH path = allShortestPaths( (vp)-[*]-(dt) ) +RETURN path +``` +produces the following result_ +![Neo4j - Graph - Result]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-graph.png) + +### Apply Zeppelin Dynamic Forms +You can leverage [Zeppelin Dynamic Form](../usage/dynamic_form/intro.html) inside your queries. This query: + +```bash +%neo4j +MATCH (o:Organization)-[r]-() +RETURN o.name, count(*), collect(distinct type(r)) AS types +ORDER BY count(*) DESC +LIMIT ${Show top=10} +``` + +produces the following result: +![Neo4j - Zeppelin - Dynamic Forms]({{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/neo4j-dynamic-forms.png) + diff --git a/neo4j/pom.xml b/neo4j/pom.xml new file mode 100644 index 00000000000..298726fb82b --- /dev/null +++ b/neo4j/pom.xml @@ -0,0 +1,144 @@ + + + + + 4.0.0 + + + zeppelin + org.apache.zeppelin + 0.8.0-SNAPSHOT + .. + + + org.apache.zeppelin + zeppelin-neo4j + jar + 0.8.0-SNAPSHOT + Zeppelin: Neo4j interpreter + + + 1.4.3 + 3.2.3 + 3.2.3 + 2.8.9 + + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + provided + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + org.neo4j.driver + neo4j-java-driver + ${neo4j.driver.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + org.neo4j.test + neo4j-harness + ${neo4j.version} + test + + + + + + + maven-enforcer-plugin + 1.3.1 + + + enforce + none + + + + + + maven-dependency-plugin + 2.8 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/../../interpreter/neo4j + false + false + true + runtime + + + + copy-artifact + package + + copy + + + ${project.build.directory}/../../interpreter/neo4j + false + false + true + runtime + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + + + + + + + + diff --git a/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jConnectionManager.java b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jConnectionManager.java new file mode 100644 index 00000000000..7cd504ef200 --- /dev/null +++ b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jConnectionManager.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.graph.neo4j; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePool; +import org.neo4j.driver.v1.AuthToken; +import org.neo4j.driver.v1.AuthTokens; +import org.neo4j.driver.v1.Config; +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.GraphDatabase; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Neo4j connection manager for Zeppelin. + */ +public class Neo4jConnectionManager { + static final Logger LOGGER = LoggerFactory.getLogger(Neo4jConnectionManager.class); + + public static final String NEO4J_SERVER_URL = "neo4j.url"; + public static final String NEO4J_AUTH_TYPE = "neo4j.auth.type"; + public static final String NEO4J_AUTH_USER = "neo4j.auth.user"; + public static final String NEO4J_AUTH_PASSWORD = "neo4j.auth.password"; + public static final String NEO4J_MAX_CONCURRENCY = "neo4j.max.concurrency"; + + private static final Pattern PROPERTY_PATTERN = Pattern.compile("\\{\\w+\\}"); + private static final String REPLACE_CURLY_BRACKETS = "\\{|\\}"; + + private static final Pattern $_PATTERN = Pattern.compile("\\$\\w+\\}"); + private static final String REPLACE_$ = "\\$"; + + private Driver driver = null; + + private final String neo4jUrl; + + private final Config config; + + private final AuthToken authToken; + + /** + * + * Enum type for the AuthToken + * + */ + public enum Neo4jAuthType {NONE, BASIC} + + public Neo4jConnectionManager(Properties properties) { + this.neo4jUrl = properties.getProperty(NEO4J_SERVER_URL); + this.config = Config.build() + .withMaxIdleSessions(Integer.parseInt(properties.getProperty(NEO4J_MAX_CONCURRENCY))) + .toConfig(); + String authType = properties.getProperty(NEO4J_AUTH_TYPE); + switch (Neo4jAuthType.valueOf(authType.toUpperCase())) { + case BASIC: + String username = properties.getProperty(NEO4J_AUTH_USER); + String password = properties.getProperty(NEO4J_AUTH_PASSWORD); + this.authToken = AuthTokens.basic(username, password); + break; + case NONE: + LOGGER.debug("Creating NONE authentication"); + this.authToken = AuthTokens.none(); + break; + default: + throw new RuntimeException("Neo4j authentication type not supported"); + } + } + + private Driver getDriver() { + if (driver == null) { + driver = GraphDatabase.driver(this.neo4jUrl, this.authToken, this.config); + } + return driver; + } + + public void open() { + getDriver(); + } + + public void close() { + getDriver().close(); + } + + private Session getSession() { + return getDriver().session(); + } + + public StatementResult execute(String cypherQuery, + InterpreterContext interpreterContext) { + Map params = new HashMap<>(); + if (interpreterContext != null) { + ResourcePool resourcePool = interpreterContext.getResourcePool(); + Set keys = extractParams(cypherQuery, PROPERTY_PATTERN, REPLACE_CURLY_BRACKETS); + keys.addAll(extractParams(cypherQuery, $_PATTERN, REPLACE_$)); + for (String key : keys) { + Resource resource = resourcePool.get(key); + if (resource != null) { + params.put(key, resource.get()); + } + } + } + LOGGER.debug("Executing cypher query {} with params {}", cypherQuery, params); + StatementResult result; + try (Session session = getSession()) { + result = params.isEmpty() + ? getSession().run(cypherQuery) : getSession().run(cypherQuery, params); + } + return result; + } + + public StatementResult execute(String cypherQuery) { + return execute(cypherQuery, null); + } + + private Set extractParams(String cypherQuery, Pattern pattern, String replaceChar) { + Matcher matcher = pattern.matcher(cypherQuery); + Set keys = new HashSet<>(); + while (matcher.find()) { + keys.add(matcher.group().replaceAll(replaceChar, StringUtils.EMPTY)); + } + return keys; + } + +} diff --git a/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreter.java b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreter.java new file mode 100644 index 00000000000..a6255225587 --- /dev/null +++ b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreter.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.graph.neo4j; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.graph.neo4j.utils.Neo4jConversionUtils; +import org.apache.zeppelin.interpreter.Interpreter; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.graph.GraphResult; +import org.apache.zeppelin.scheduler.Scheduler; +import org.apache.zeppelin.scheduler.SchedulerFactory; +import org.neo4j.driver.internal.types.InternalTypeSystem; +import org.neo4j.driver.internal.util.Iterables; +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.types.Relationship; +import org.neo4j.driver.v1.types.TypeSystem; +import org.neo4j.driver.v1.util.Pair; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Neo4j interpreter for Zeppelin. + */ +public class Neo4jCypherInterpreter extends Interpreter { + private static final String TABLE = "%table"; + public static final String NEW_LINE = "\n"; + public static final String TAB = "\t"; + + private static final String MAP_KEY_TEMPLATE = "%s.%s"; + + private Map labels; + + private Set types; + + private final Neo4jConnectionManager neo4jConnectionManager; + + private final ObjectMapper jsonMapper = new ObjectMapper(); + + public Neo4jCypherInterpreter(Properties properties) { + super(properties); + this.neo4jConnectionManager = new Neo4jConnectionManager(properties); + } + + @Override + public void open() { + this.neo4jConnectionManager.open(); + } + + @Override + public void close() { + this.neo4jConnectionManager.close(); + } + + public Map getLabels(boolean refresh) { + if (labels == null || refresh) { + Map old = labels == null ? + new LinkedHashMap() : new LinkedHashMap<>(labels); + labels = new LinkedHashMap<>(); + StatementResult result = this.neo4jConnectionManager.execute("CALL db.labels()"); + Set colors = new HashSet<>(); + while (result.hasNext()) { + Record record = result.next(); + String label = record.get("label").asString(); + String color = old.get(label); + while (color == null || colors.contains(color)) { + color = Neo4jConversionUtils.getRandomLabelColor(); + } + colors.add(color); + labels.put(label, color); + } + } + return labels; + } + + private Set getTypes(boolean refresh) { + if (types == null || refresh) { + types = new HashSet<>(); + StatementResult result = this.neo4jConnectionManager.execute("CALL db.relationshipTypes()"); + while (result.hasNext()) { + Record record = result.next(); + types.add(record.get("relationshipType").asString()); + } + } + return types; + } + + @Override + public InterpreterResult interpret(String cypherQuery, InterpreterContext interpreterContext) { + logger.info("Opening session"); + if (StringUtils.isBlank(cypherQuery)) { + return new InterpreterResult(Code.SUCCESS); + } + try { + StatementResult result = this.neo4jConnectionManager.execute(cypherQuery, + interpreterContext); + Set nodes = new HashSet<>(); + Set relationships = new HashSet<>(); + List columns = new ArrayList<>(); + List> lines = new ArrayList>(); + while (result.hasNext()) { + Record record = result.next(); + List> fields = record.fields(); + List line = new ArrayList<>(); + for (Pair field : fields) { + if (field.value().hasType(InternalTypeSystem.TYPE_SYSTEM.NODE())) { + nodes.add(field.value().asNode()); + } else if (field.value().hasType(InternalTypeSystem.TYPE_SYSTEM.RELATIONSHIP())) { + relationships.add(field.value().asRelationship()); + } else if (field.value().hasType(InternalTypeSystem.TYPE_SYSTEM.PATH())) { + nodes.addAll(Iterables.asList(field.value().asPath().nodes())); + relationships.addAll(Iterables.asList(field.value().asPath().relationships())); + } else { + setTabularResult(field.key(), field.value(), columns, line, + InternalTypeSystem.TYPE_SYSTEM); + } + } + if (!line.isEmpty()) { + lines.add(line); + } + } + if (!nodes.isEmpty()) { + return renderGraph(nodes, relationships); + } else { + return renderTable(columns, lines); + } + } catch (Exception e) { + logger.error("Exception while interpreting cypher query", e); + return new InterpreterResult(Code.ERROR, e.getMessage()); + } + } + + private void setTabularResult(String key, Object obj, List columns, List line, + TypeSystem typeSystem) { + if (obj instanceof Value) { + Value value = (Value) obj; + if (value.hasType(typeSystem.MAP())) { + Map map = value.asMap(); + for (Entry entry : map.entrySet()) { + setTabularResult(String.format(MAP_KEY_TEMPLATE, key, entry.getKey()), entry.getValue(), + columns, line, typeSystem); + } + } else { + addValueToLine(key, columns, line, value); + } + } else if (obj instanceof Map) { + Map map = (Map) obj; + for (Entry entry : map.entrySet()) { + setTabularResult(String.format(MAP_KEY_TEMPLATE, key, entry.getKey()), entry.getValue(), + columns, line, typeSystem); + } + } else { + addValueToLine(key, columns, line, obj); + } + } + + private void addValueToLine(String key, List columns, List line, Object value) { + if (!columns.contains(key)) { + columns.add(key); + } + int position = columns.indexOf(key); + if (line.size() < columns.size()) { + for (int i = line.size(); i < columns.size(); i++) { + line.add(null); + } + } + if (value != null) { + if (value instanceof Value) { + Value val = (Value) value; + if (val.hasType(InternalTypeSystem.TYPE_SYSTEM.LIST())) { + value = val.asList(); + } else if (val.hasType(InternalTypeSystem.TYPE_SYSTEM.MAP())) { + value = val.asMap(); + } + } + if (value instanceof Collection) { + try { + value = jsonMapper.writer().writeValueAsString(value); + } catch (Exception ignored) {} + } + } + line.set(position, value == null ? null : value.toString()); + } + + private InterpreterResult renderTable(List cols, List> lines) { + logger.info("Executing renderTable method"); + StringBuilder msg = null; + if (cols.isEmpty()) { + msg = new StringBuilder(); + } else { + msg = new StringBuilder(TABLE); + msg.append(NEW_LINE); + msg.append(StringUtils.join(cols, TAB)); + msg.append(NEW_LINE); + for (List line : lines) { + if (line.size() < cols.size()) { + for (int i = line.size(); i < cols.size(); i++) { + line.add(null); + } + } + msg.append(StringUtils.join(line, TAB)); + msg.append(NEW_LINE); + } + } + return new InterpreterResult(Code.SUCCESS, msg.toString()); + } + + private InterpreterResult renderGraph(Set nodes, + Set relationships) { + logger.info("Executing renderGraph method"); + List nodesList = new ArrayList<>(); + List relsList = new ArrayList<>(); + for (Relationship rel : relationships) { + relsList.add(Neo4jConversionUtils.toZeppelinRelationship(rel)); + } + Map labels = getLabels(true); + for (Node node : nodes) { + nodesList.add(Neo4jConversionUtils.toZeppelinNode(node, labels)); + } + return new GraphResult(Code.SUCCESS, + new GraphResult.Graph(nodesList, relsList, labels, getTypes(true), true)); + } + + @Override + public Scheduler getScheduler() { + return SchedulerFactory.singleton() + .createOrGetParallelScheduler(Neo4jCypherInterpreter.class.getName() + this.hashCode(), + Integer.parseInt(getProperty(Neo4jConnectionManager.NEO4J_MAX_CONCURRENCY))); + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } + + @Override + public FormType getFormType() { + return FormType.SIMPLE; + } + + @Override + public void cancel(InterpreterContext context) { + } + +} diff --git a/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/utils/Neo4jConversionUtils.java b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/utils/Neo4jConversionUtils.java new file mode 100644 index 00000000000..484940198a7 --- /dev/null +++ b/neo4j/src/main/java/org/apache/zeppelin/graph/neo4j/utils/Neo4jConversionUtils.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.graph.neo4j.utils; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.types.Relationship; + +/** + * Neo4jConversionUtils + */ +public class Neo4jConversionUtils { + private Neo4jConversionUtils() {} + + private static final String[] LETTERS = "0123456789ABCDEF".split(""); + + public static final String COLOR_GREY = "#D3D3D3"; + + public static org.apache.zeppelin.tabledata.Node toZeppelinNode(Node n, + Map graphLabels) { + Set labels = new LinkedHashSet<>(); + String firstLabel = null; + for (String label : n.labels()) { + if (firstLabel == null) { + firstLabel = label; + } + labels.add(label); + } + return new org.apache.zeppelin.tabledata.Node(n.id(), n.asMap(), + labels); + } + + public static org.apache.zeppelin.tabledata.Relationship + toZeppelinRelationship(Relationship r) { + return new org.apache.zeppelin.tabledata.Relationship(r.id(), r.asMap(), + r.startNodeId(), r.endNodeId(), r.type()); + } + + public static String getRandomLabelColor() { + char[] color = new char[7]; + color[0] = '#'; + for (int i = 1; i < color.length; i++) { + color[i] = LETTERS[(int) Math.floor(Math.random() * 16)].charAt(0); + } + return new String(color); + } + +} diff --git a/neo4j/src/main/resources/interpreter-setting.json b/neo4j/src/main/resources/interpreter-setting.json new file mode 100644 index 00000000000..8db4367cc3e --- /dev/null +++ b/neo4j/src/main/resources/interpreter-setting.json @@ -0,0 +1,42 @@ +[ + { + "group": "neo4j", + "name": "neo4j", + "className": "org.apache.zeppelin.graph.neo4j.Neo4jCypherInterpreter", + "properties": { + "neo4j.url": { + "envName": null, + "propertyName": "neo4j.url", + "defaultValue": "bolt://localhost:7687", + "description": "The Neo4j's BOLT url." + }, + "neo4j.auth.type": { + "envName": null, + "propertyName": "neo4j.auth.type", + "defaultValue": "BASIC", + "description": "The Neo4j's authentication type (NONE, BASIC)." + }, + "neo4j.auth.user": { + "envName": null, + "propertyName": "neo4j.auth.user", + "defaultValue": "", + "description": "The Neo4j user name." + }, + "neo4j.auth.password": { + "envName": null, + "propertyName": "neo4j.auth.password", + "defaultValue": "", + "description": "The Neo4j user password." + }, + "neo4j.max.concurrency": { + "envName": null, + "propertyName": "neo4j.max.concurrency", + "defaultValue": "50", + "description": "Max concurrency call from Zeppelin to Neo4j server." + } + }, + "editor": { + "editOnDblClick": false + } + } +] diff --git a/neo4j/src/test/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreterTest.java b/neo4j/src/test/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreterTest.java new file mode 100644 index 00000000000..1bb14b71e1f --- /dev/null +++ b/neo4j/src/test/java/org/apache/zeppelin/graph/neo4j/Neo4jCypherInterpreterTest.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.zeppelin.graph.neo4j; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; +import org.apache.zeppelin.display.AngularObjectRegistry; +import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.graph.neo4j.Neo4jConnectionManager.Neo4jAuthType; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.apache.zeppelin.interpreter.InterpreterContextRunner; +import org.apache.zeppelin.interpreter.InterpreterGroup; +import org.apache.zeppelin.interpreter.InterpreterOutput; +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.interpreter.InterpreterResult.Code; +import org.apache.zeppelin.interpreter.graph.GraphResult; +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.neo4j.harness.ServerControls; +import org.neo4j.harness.TestServerBuilders; + +import com.google.gson.Gson; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class Neo4jCypherInterpreterTest { + + private Neo4jCypherInterpreter interpreter; + + private InterpreterContext context; + + private static ServerControls server; + + private static final Gson gson = new Gson(); + + private static final String LABEL_PERSON = "Person"; + private static final String REL_KNOWS = "KNOWS"; + + private static final String CYPHER_FOREACH = "FOREACH (x in range(1,1000) | CREATE (:%s{name: \"name\" + x, age: %s}))"; + private static final String CHPHER_UNWIND = "UNWIND range(1,1000) as x " + + "MATCH (n), (m) WHERE id(n) = x AND id(m) = toInt(rand() * 1000) " + + "CREATE (n)-[:%s]->(m)"; + + @BeforeClass + public static void setUpNeo4jServer() throws Exception { + server = TestServerBuilders.newInProcessBuilder() + .withConfig("dbms.security.auth_enabled","false") + .withFixture(String.format(CYPHER_FOREACH, LABEL_PERSON, "x % 10")) + .withFixture(String.format(CHPHER_UNWIND, REL_KNOWS)) + .newServer(); + } + + @AfterClass + public static void tearDownNeo4jServer() throws Exception { + server.close(); + } + + @Before + public void setUpZeppelin() { + Properties p = new Properties(); + p.setProperty(Neo4jConnectionManager.NEO4J_SERVER_URL, server.boltURI().toString()); + p.setProperty(Neo4jConnectionManager.NEO4J_AUTH_TYPE, Neo4jAuthType.NONE.toString()); + p.setProperty(Neo4jConnectionManager.NEO4J_MAX_CONCURRENCY, "50"); + interpreter = new Neo4jCypherInterpreter(p); + context = new InterpreterContext("note", "id", null, "title", "text", + new AuthenticationInfo(), + new HashMap(), + new GUI(), + new AngularObjectRegistry(new InterpreterGroup().getId(), null), + new LocalResourcePool("id"), + new LinkedList(), + new InterpreterOutput(null)); + } + + @After + public void tearDownZeppelin() throws Exception { + interpreter.close(); + } + + @Test + public void testTableWithArray() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("return 'a' as colA, 'b' as colB, [1, 2, 3] as colC", context); + assertEquals(Code.SUCCESS, result.code()); + final String tableResult = "colA\tcolB\tcolC\n\"a\"\t\"b\"\t[1,2,3]\n"; + assertEquals(tableResult, result.toString().replace("%table ", StringUtils.EMPTY)); + + result = interpreter.interpret("return 'a' as colA, 'b' as colB, [{key: \"value\"}, {key: 1}] as colC", context); + assertEquals(Code.SUCCESS, result.code()); + final String tableResultWithMap = "colA\tcolB\tcolC\n\"a\"\t\"b\"\t[{\"key\":\"value\"},{\"key\":1}]\n"; + assertEquals(tableResultWithMap, result.toString().replace("%table ", StringUtils.EMPTY)); + } + + @Test + public void testCreateIndex() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("CREATE INDEX ON :Person(name)", context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals(StringUtils.EMPTY, result.toString()); + } + + @Test + public void testRenderTable() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("MATCH (n:Person) " + + "WHERE n.name IN ['name1', 'name2', 'name3'] " + + "RETURN n.name AS name, n.age AS age", context); + assertEquals(Code.SUCCESS, result.code()); + final String tableResult = "name\tage\n\"name1\"\t1\n\"name2\"\t2\n\"name3\"\t3\n"; + assertEquals(tableResult, result.toString().replace("%table ", StringUtils.EMPTY)); + } + + @Test + public void testRenderMap() { + interpreter.open(); + final String jsonQuery = "RETURN {key: \"value\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]} as object"; + final String objectKey = "object.key"; + final String objectListKey = "object.listKey"; + InterpreterResult result = interpreter.interpret(jsonQuery, context); + assertEquals(Code.SUCCESS, result.code()); + String[] rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 2); + List header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + List row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + + final String query = "WITH [{key: \"value\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]}," + + "{key: \"value2\", listKey: [{inner: \"Map12\"}, {inner: \"Map22\"}]}] " + + "AS array UNWIND array AS object RETURN object"; + result = interpreter.interpret(query, context); + assertEquals(Code.SUCCESS, result.code()); + rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 3); + header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + row = Arrays.asList(rows[2].split(Neo4jCypherInterpreter.TAB)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value2"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map12\"},{\"inner\":\"Map22\"}]"); + + final String jsonListWithNullQuery = "WITH [{key: \"value\", listKey: null}," + + "{key: \"value2\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]}] " + + "AS array UNWIND array AS object RETURN object"; + result = interpreter.interpret(jsonListWithNullQuery, context); + assertEquals(Code.SUCCESS, result.code()); + rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 3); + header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), StringUtils.EMPTY); + assertEquals(row.get(header.indexOf(objectListKey)), ""); + row = Arrays.asList(rows[2].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value2"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + + final String jsonListWithoutListKeyQuery = "WITH [{key: \"value\"}," + + "{key: \"value2\", listKey: [{inner: \"Map1\"}, {inner: \"Map2\"}]}] " + + "AS array UNWIND array AS object RETURN object"; + result = interpreter.interpret(jsonListWithoutListKeyQuery, context); + assertEquals(Code.SUCCESS, result.code()); + rows = result.toString().replace("%table ", StringUtils.EMPTY).split(Neo4jCypherInterpreter.NEW_LINE); + assertEquals(rows.length, 3); + header = Arrays.asList(rows[0].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(header.contains(objectKey), true); + assertEquals(header.contains(objectListKey), true); + row = Arrays.asList(rows[1].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value"); + assertEquals(row.get(header.indexOf(objectListKey)), StringUtils.EMPTY); + row = Arrays.asList(rows[2].split(Neo4jCypherInterpreter.TAB, -1)); + assertEquals(row.size(), header.size()); + assertEquals(row.get(header.indexOf(objectKey)), "value2"); + assertEquals(row.get(header.indexOf(objectListKey)), "[{\"inner\":\"Map1\"},{\"inner\":\"Map2\"}]"); + } + + @Test + public void testRenderNetwork() { + interpreter.open(); + InterpreterResult result = interpreter.interpret("MATCH (n)-[r:KNOWS]-(m) RETURN n, r, m LIMIT 1", context); + GraphResult.Graph graph = gson.fromJson(result.toString().replace("%network ", StringUtils.EMPTY), GraphResult.Graph.class); + assertEquals(2, graph.getNodes().size()); + assertEquals(true, graph.getNodes().iterator().next().getLabel().equals(LABEL_PERSON)); + assertEquals(1, graph.getEdges().size()); + assertEquals(true, graph.getEdges().iterator().next().getLabel().equals(REL_KNOWS)); + assertEquals(1, graph.getLabels().size()); + assertEquals(1, graph.getTypes().size()); + assertEquals(true, graph.getLabels().containsKey(LABEL_PERSON)); + assertEquals(REL_KNOWS, graph.getTypes().iterator().next()); + assertEquals(Code.SUCCESS, result.code()); + } + + @Test + public void testFallingQuery() { + interpreter.open(); + final String ERROR_MSG_EMPTY = ""; + InterpreterResult result = interpreter.interpret(StringUtils.EMPTY, context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals(ERROR_MSG_EMPTY, result.toString()); + + result = interpreter.interpret(null, context); + assertEquals(Code.SUCCESS, result.code()); + assertEquals(ERROR_MSG_EMPTY, result.toString()); + + result = interpreter.interpret("MATCH (n:Person{name: }) RETURN n.name AS name, n.age AS age", context); + assertEquals(Code.ERROR, result.code()); + } + +} diff --git a/pom.xml b/pom.xml index acfcd0572c0..7ff9acd7ae0 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,7 @@ bigquery alluxio scio + neo4j zeppelin-web zeppelin-server zeppelin-jupyter diff --git a/zeppelin-distribution/src/bin_license/LICENSE b/zeppelin-distribution/src/bin_license/LICENSE index 3a6d0aac881..42302169d23 100644 --- a/zeppelin-distribution/src/bin_license/LICENSE +++ b/zeppelin-distribution/src/bin_license/LICENSE @@ -217,6 +217,7 @@ The following components are provided under Apache License. (Apache 2.0) frontend-maven-plugin 1.3 (com.github.eirslett:frontend-maven-plugin:1.3 - https://github.com/eirslett/frontend-maven-plugin/blob/frontend-plugins-1.3/LICENSE (Apache 2.0) frontend-plugin-core 1.3 (com.github.eirslett:frontend-plugin-core) - https://github.com/eirslett/frontend-maven-plugin/blob/frontend-plugins-1.3/LICENSE (Apache 2.0) mongo-java-driver 3.4.1 (org.mongodb:mongo-java-driver:3.4.1) - https://github.com/mongodb/mongo-java-driver/blob/master/LICENSE.txt + (Apache 2.0) Neo4j Java Driver (https://github.com/neo4j/neo4j-java-driver) - https://github.com/neo4j/neo4j-java-driver/blob/1.4.3/LICENSE.txt ======================================================================== MIT licenses diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/graph/GraphResult.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/graph/GraphResult.java new file mode 100644 index 00000000000..df1b9a3ae6e --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/graph/GraphResult.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.interpreter.graph; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.apache.zeppelin.interpreter.InterpreterResult; +import org.apache.zeppelin.tabledata.Node; +import org.apache.zeppelin.tabledata.Relationship; + +import com.google.gson.Gson; + +/** + * The intepreter result template for Networks + * + */ +public class GraphResult extends InterpreterResult { + + /** + * The Graph structure parsed from the front-end + * + */ + public static class Graph { + private Collection nodes; + + private Collection edges; + + /** + * The node types in the whole graph, and the related colors + * + */ + private Map labels; + + /** + * The relationship types in the whole graph + * + */ + private Set types; + + /** + * Is a directed graph + */ + private boolean directed; + + public Graph() {} + + public Graph(Collection nodes, Collection edges, + Map labels, Set types, boolean directed) { + super(); + this.setNodes(nodes); + this.setEdges(edges); + this.setLabels(labels); + this.setTypes(types); + this.setDirected(directed); + } + + public Collection getNodes() { + return nodes; + } + + public void setNodes(Collection nodes) { + this.nodes = nodes; + } + + public Collection getEdges() { + return edges; + } + + public void setEdges(Collection edges) { + this.edges = edges; + } + + public Map getLabels() { + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + public Set getTypes() { + return types; + } + + public void setTypes(Set types) { + this.types = types; + } + + public boolean isDirected() { + return directed; + } + + public void setDirected(boolean directed) { + this.directed = directed; + } + + } + + private static final Gson gson = new Gson(); + + public GraphResult(Code code, Graph graphObject) { + super(code, Type.NETWORK, gson.toJson(graphObject)); + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/GraphEntity.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/GraphEntity.java new file mode 100644 index 00000000000..320b1447269 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/GraphEntity.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.tabledata; + +import java.util.Map; + +/** + * The base network entity + * + */ +public abstract class GraphEntity { + + private long id; + + /** + * The data of the entity + * + */ + private Map data; + + /** + * The primary type of the entity + */ + private String label; + + public GraphEntity() {} + + public GraphEntity(long id, Map data, String label) { + super(); + this.setId(id); + this.setData(data); + this.setLabel(label); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Node.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Node.java new file mode 100644 index 00000000000..2efabc40894 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Node.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.tabledata; + +import java.util.Map; +import java.util.Set; + +/** + * The Zeppelin Node Entity + * + */ +public class Node extends GraphEntity { + + /** + * The labels (types) attached to a node + */ + private Set labels; + + public Node() {} + + + public Node(long id, Map data, Set labels) { + super(id, data, labels.iterator().next()); + } + + public Set getLabels() { + return labels; + } + + public void setLabels(Set labels) { + this.labels = labels; + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Relationship.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Relationship.java new file mode 100644 index 00000000000..aa8ddb7854a --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/tabledata/Relationship.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.zeppelin.tabledata; + +import java.util.Map; + +/** + * The Zeppelin Relationship entity + * + */ +public class Relationship extends GraphEntity { + + /** + * Source node ID + */ + private long source; + + /** + * End node ID + */ + private long target; + + public Relationship() {} + + public Relationship(long id, Map data, long source, + long target, String label) { + super(id, data, label); + this.setSource(source); + this.setTarget(target); + } + + public long getSource() { + return source; + } + + public void setSource(long startNodeId) { + this.source = startNodeId; + } + + public long getTarget() { + return target; + } + + public void setTarget(long endNodeId) { + this.target = endNodeId; + } + +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 2dec19cbf56..6ccd272c910 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -623,7 +623,8 @@ public static enum ConfVars { + "org.apache.zeppelin.bigquery.BigQueryInterpreter," + "org.apache.zeppelin.beam.BeamInterpreter," + "org.apache.zeppelin.scio.ScioInterpreter," - + "org.apache.zeppelin.groovy.GroovyInterpreter" + + "org.apache.zeppelin.groovy.GroovyInterpreter," + + "org.apache.zeppelin.neo4j.Neo4jCypherInterpreter" ), ZEPPELIN_INTERPRETER_JSON("zeppelin.interpreter.setting", "interpreter-setting.json"), ZEPPELIN_INTERPRETER_DIR("zeppelin.interpreter.dir", "interpreter"), @@ -634,7 +635,7 @@ public static enum ConfVars { ZEPPELIN_INTERPRETER_MAX_POOL_SIZE("zeppelin.interpreter.max.poolsize", 10), ZEPPELIN_INTERPRETER_GROUP_ORDER("zeppelin.interpreter.group.order", "spark,md,angular,sh," + "livy,alluxio,file,psql,flink,python,ignite,lens,cassandra,geode,kylin,elasticsearch," - + "scalding,jdbc,hbase,bigquery,beam,pig,scio,groovy"), + + "scalding,jdbc,hbase,bigquery,beam,pig,scio,groovy,neo4j"), ZEPPELIN_INTERPRETER_OUTPUT_LIMIT("zeppelin.interpreter.output.limit", 1024 * 100), ZEPPELIN_ENCODING("zeppelin.encoding", "UTF-8"), ZEPPELIN_NOTEBOOK_DIR("zeppelin.notebook.dir", "notebook"),