Gremlin Java

更新时间:
复制为 MD 格式

Lindorm 图引擎兼容 Apache TinkerPop Gremlin 协议,支持通过官方 gremlin-driver 在 Java 应用中提交 Gremlin 查询。本文介绍 Maven 依赖、连接方式、参数绑定与超时设置,并提供一个可直接编译运行的完整示例。

前提条件

  • 已开通 Lindorm 图引擎,并配置白名单。

  • 已完成图的 Schema 初始化,如未完成可参考Schema 定义

  • 本地已安装 JDK 8+ 和 Maven 3.6+。

Maven 依赖

在项目的 pom.xml 中加入 gremlin-driver 依赖。Lindorm 图引擎兼容 TinkerPop 3.7 及以上版本,建议使用 3.8.x:

<dependency>
  <groupId>org.apache.tinkerpop</groupId>
  <artifactId>gremlin-driver</artifactId>
  <version>3.8.1</version>
</dependency>

连接方式

Lindorm 图引擎支持两种常见的连接方式:通过 YAML 配置文件加载,或直接在代码中构造 Cluster 对象。生产环境建议使用 YAML 文件,便于将连接参数与代码解耦。

方式一:YAML 配置

创建 remote.yaml

hosts: [实例名称-proxy-graph-vpc.lindorm.aliyuncs.com]
port: 16032
username: 账号
password: 密码
path: /gremlin/子图名称
connectionPool: {  
  maxSize: 16,  
  minSize: 16,  
  maxInProcessPerConnection: 4,  
  maxContentLength: ${yourExpectedSize}
}
serializer: { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false }}

主要字段说明:

  • ${yourExpectedSize}: 默认为65536,如果您的结果集过大,有可能会遇到Max frame length of 65536 has been exceeded 该类问题,根据业务调整该值大小便可解决该问题。

  • maxSizeminSize:客户端具有链接池,一个客户端支持多线程并发访问,需要把这两个参数设置成一样,且大于等于您的客户端线程访问数,这样也能避免Timed out while waiting for an available host 错误。

  • 序列化协议(尽量使用GraphBinaryMessageSerializerV1序列化协议):

    • GraphBinaryMessageSerializerV1序列化协议:返回的Vertex、EdgeReferenceVertex,ReferenceEdge,序列化时不会返回properties。性能好。

    • GraphSONMessageSerializerV3d0序列化协议:返回的 Vertex、EdgeDetachedVertex,DetachedEdge,序列化时会返回所有的properties,因此在Vertex可能存在大量properties时,会增加很多额外的序列化开销。

在 Java 代码中加载该配置并创建 Cluster

import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;

Cluster cluster = Cluster.open("config/remote.yaml");
Client client = cluster.connect();
client.init();

// ... 执行操作 ...

client.close();
cluster.close();

方式二:代码构造 Cluster

import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;

Cluster cluster = Cluster.build("ld-xx-proxy-graph-pub.lindorm.aliyuncs.com")
        .port(16032)
        .serializer(Serializers.GRAPHBINARY_V1)
        .maxConnectionPoolSize(32)
        .minConnectionPoolSize(32)
        .credentials("root", "xx")
        .path("/gremlin/default")
        .create();
Client client = cluster.connect();
client.init();

// ... 执行操作 ...

client.close();
cluster.close();

参数绑定与超时设置

Java 客户端通过 RequestOptions.addParameter 将变量传入查询语句,避免拼接字符串,提升安全性和可读性。

import org.apache.tinkerpop.gremlin.driver.RequestOptions;
import org.apache.tinkerpop.gremlin.driver.Result;
import org.apache.tinkerpop.gremlin.driver.ResultSet;

String dsl = "g.V(G___id).hasLabel(G___label)";
RequestOptions.Builder options = RequestOptions.build().timeout(30000);
options.addParameter("G___id", "marko");
options.addParameter("G___label", "person");

ResultSet resultSet = client.submit(dsl, options.create());
List<Result> results = resultSet.all().join();
for (Result r : results) {
    System.out.println(r.getObject());
}
//执行client关闭等操作
说明

绑定变量名推荐使用 G___ 前缀加字段名的格式(如 G___idG___label),在 DSL 中直接引用变量名即可,无需加引号。

完整示例

以下示例演示如何创建一个独立的 Maven 项目,连接 Lindorm 图引擎并执行 Gremlin 查询,包含初始化项目、配置依赖、编写主类、编译运行四个步骤。示例中集成的向量检索能力为可选模块,若业务无相关需求可直接移除。

初始化项目目录

mkdir lindorm-graph-java-demo
cd lindorm-graph-java-demo
mkdir -p config
mkdir -p src/main/java/com/aliyun/lindorm/demo

创建 config/remote.yaml

hosts: [实例名称-proxy-graph-vpc.lindorm.aliyuncs.com]
port: 16032
username: 账号
password: 密码
path: /gremlin/子图名称
connectionPool: {  
  maxSize: 16,  
  minSize: 16,  
  maxInProcessPerConnection: 4,  
  maxContentLength: ${yourExpectedSize}
}
serializer: { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false }}

创建 pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.aliyun.lindorm</groupId>
    <artifactId>lindorm-graph-java-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <slf4j.version>2.0.17</slf4j.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>jcl-over-slf4j</artifactId>
                <version>${slf4j.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.tinkerpop</groupId>
            <artifactId>gremlin-driver</artifactId>
            <version>3.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.aliyun.lindorm.demo.LindormGraphExampleDemo</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

编写 LindormGraphExampleDemo.java

src/main/java/com/example/ 目录下新建 LindormGraphExampleDemo.java

package com.aliyun.lindorm.demo;

import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.driver.Result;
import org.apache.tinkerpop.gremlin.driver.ResultSet;
import org.apache.tinkerpop.gremlin.driver.RequestOptions;
import org.apache.tinkerpop.gremlin.util.ser.Serializers;
import org.yaml.snakeyaml.Yaml;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * 图数据库 Java 客户端示例(对应 example_client.py)
 * 1. 调用 /schema/mgmt/apply 初始化 Schema(含向量属性)
 * 2. 插入顶点(person 含128维向量、software)和边(knows、created)
 * 3. 查询顶点、图遍历、向量检索
 */
public class LindormGraphExampleDemo {

    private static final int REQUEST_TIMEOUT_MS = 30000;
    private static final int VECTOR_DIM = 128;
    private static final Random RANDOM = new Random();

    // ========== Schema JSON ==========
    // 如果不使用向量,请去掉 embedding 向量列
    private static final String SCHEMA_JSON =
            "{\"vertexLabels\":["
            + "{\"label\":\"person\",\"properties\":["
            +   "{\"name\":\"name\",\"dataType\":\"STRING\"},"
            +   "{\"name\":\"age\",\"dataType\":\"INT\"},"
            +   "{\"name\":\"city\",\"dataType\":\"STRING\"},"
            +   "{\"name\":\"embedding\",\"dataType\":\"VECTOR_FLOAT\","
            +     "\"vectorMeta\":{\"dimension\":128,\"distanceMethod\":\"EUCLIDEAN\","
            +     "\"indexType\":\"HNSW\",\"indexParams\":{\"M\":\"24\",\"EF_CONSTRUCT\":\"200\"}}}"
            + "]},"
            + "{\"label\":\"software\",\"properties\":["
            +   "{\"name\":\"name\",\"dataType\":\"STRING\"},"
            +   "{\"name\":\"lang\",\"dataType\":\"STRING\"},"
            +   "{\"name\":\"price\",\"dataType\":\"INT\"}"
            + "]}"
            + "],\"edgeLabels\":["
            + "{\"label\":\"knows\",\"properties\":["
            +   "{\"name\":\"date\",\"dataType\":\"STRING\"},"
            +   "{\"name\":\"weight\",\"dataType\":\"DOUBLE\"}"
            + "]},"
            + "{\"label\":\"created\",\"properties\":["
            +   "{\"name\":\"date\",\"dataType\":\"STRING\"},"
            +   "{\"name\":\"weight\",\"dataType\":\"DOUBLE\"}"
            + "]}"
            + "],\"connections\":["
            + "{\"edgeLabel\":\"knows\",\"outVertex\":\"person\",\"inVertex\":\"person\"},"
            + "{\"edgeLabel\":\"created\",\"outVertex\":\"person\",\"inVertex\":\"software\"}"
            + "]}";

    // ========== Gremlin DSL ==========
    private static final String ADD_PERSON_DSL =
            "g.addV(G___label).property(id, G___id)"
            + ".property('name', G___name)"
            + ".property('age', G___age)"
            + ".property('city', G___city)"
            + ".property('embedding', G___embedding)";

    private static final String ADD_SOFTWARE_DSL =
            "g.addV(G___label).property(id, G___id)"
            + ".property('name', G___name)"
            + ".property('lang', G___lang)"
            + ".property('price', G___price)";

    private static final String ADD_EDGE_DSL =
            "g.V(G___fromId).hasLabel(G___fromLabel).addE(G___edgeLabel).to(__.V(G___toId).hasLabel(G___toLabel))"
            + ".property('date', G___date)"
            + ".property('weight', G___weight)";

    private static final String QUERY_VERTEX_DSL =
            "g.V(G___id).hasLabel(G___label).valueMap(true)";

    private static final String QUERY_NEIGHBORS_DSL =
            "g.V(G___id).hasLabel(G___label).out(G___edgeLabel).valueMap(true)";

    private static final String VECTOR_SEARCH_DSL =
            "g.V().hasLabel(G___label).hasVector(G___prop, G___vector, G___topK).valueMap(true)";

    // ========== main ==========

    public static void main(String[] args) throws Exception {
        String yamlPath = args.length > 0 ? args[0] : "config/remote.yaml";
        Config config = loadConfig(yamlPath);

        // 1. 初始化 Schema
        printSection("1. 初始化 Schema");
        applySchema(config);

        // 2. 创建 Gremlin 客户端
        Cluster cluster = null;

        Client client = null;
        try {
            // 客户端包含连接池,线程安全,支持多线程并发
            cluster = Cluster.build(new File(yamlPath)).create();
//            cluster = Cluster.build("ld-xx-proxy-graph-pub.lindorm.aliyuncs.com")
//                    .port(16032)
//                    .serializer(Serializers.GRAPHBINARY_V1)
//                    .maxConnectionPoolSize(32)
//                    .minConnectionPoolSize(32)
//                    .credentials("root", "xx")
//                    .path("/gremlin/default")
//                    .create();

            client = cluster.connect().init();

            // 3. 插入数据
            // 等待 Schema(含向量索引)在服务端构建完成,避免后续插入/检索时索引尚未就绪
            Thread.sleep(10000);

            printSection("2. 插入顶点和边");

            System.out.println("\n--- 添加 person 顶点 ---");
            addPersons(client);

            System.out.println("\n--- 添加 software 顶点 ---");
            addSoftwares(client);

            System.out.println("\n--- 添加边 ---");
            addEdges(client);

            // 4. 查询
            printSection("3. 查询");

            System.out.println("\n--- 查询顶点 marko ---");
            queryVertex(client, "marko", "person");

            System.out.println("\n--- marko 认识的人 ---");
            queryNeighbors(client, "marko", "person", "knows");

            System.out.println("\n--- marko 创建的软件 ---");
            queryNeighbors(client, "marko", "person", "created");

            System.out.println("\n--- 向量检索 top 3 ---");
            queryVectorSearch(client, "person", "embedding", 3);

        } finally {
            if (client != null) client.close();
            if (cluster != null) cluster.close();
        }

        System.out.println("\n完成!");
    }

    // ========== 1. Schema 初始化 ==========

    private static void applySchema(Config config) throws Exception {
        String url = config.schemaBaseUrl()
                + "/schema/mgmt/apply?db=" + URLEncoder.encode(config.dbName, "UTF-8");
        String result = httpPost(url, SCHEMA_JSON, config);
        System.out.println("Schema 初始化: " + result);
    }

    // ========== 2. 插入数据 ==========

    private static void addPersons(Client client) {
        String[][] persons = {
            {"marko", "marko", "29", "Beijing"},
            {"vadas", "vadas", "27", "Hongkong"},
            {"josh",  "josh",  "32", "Beijing"},
            {"peter", "peter", "35", "Shanghai"},
        };

        for (String[] p : persons) {
            Map<String, Object> bindings = new HashMap<>();
            bindings.put("G___label", "person");
            bindings.put("G___id", p[0]);
            bindings.put("G___name", p[1]);
            bindings.put("G___age", Integer.parseInt(p[2]));
            bindings.put("G___city", p[3]);
            bindings.put("G___embedding", randomVector(VECTOR_DIM));
            List<Result> results = execute(client, ADD_PERSON_DSL, bindings);
            System.out.println("添加顶点: " + p[0] + " -> " + results);
        }
    }

    private static void addSoftwares(Client client) {
        String[][] softwares = {
            {"lop",    "lop",    "java", "328"},
            {"ripple", "ripple", "java", "199"},
        };

        for (String[] s : softwares) {
            Map<String, Object> bindings = new HashMap<>();
            bindings.put("G___label", "software");
            bindings.put("G___id", s[0]);
            bindings.put("G___name", s[1]);
            bindings.put("G___lang", s[2]);
            bindings.put("G___price", Integer.parseInt(s[3]));
            List<Result> results = execute(client, ADD_SOFTWARE_DSL, bindings);
            System.out.println("添加顶点: " + s[0] + " -> " + results);
        }
    }

    private static void addEdges(Client client) {
        // {from, fromLabel, to, toLabel, edgeLabel, date, weight}
        String[][] edges = {
            {"marko", "person", "vadas",  "person",   "knows",   "20160110", "0.5"},
            {"marko", "person", "josh",   "person",   "knows",   "20130220", "1.0"},
            {"marko", "person", "lop",    "software", "created", "20171210", "0.4"},
            {"josh",  "person", "lop",    "software", "created", "20091111", "0.4"},
            {"josh",  "person", "ripple", "software", "created", "20171210", "1.0"},
            {"peter", "person", "lop",    "software", "created", "20170324", "0.2"},
        };

        for (String[] e : edges) {
            Map<String, Object> bindings = new HashMap<>();
            bindings.put("G___fromId", e[0]);
            bindings.put("G___fromLabel", e[1]);
            bindings.put("G___toId", e[2]);
            bindings.put("G___toLabel", e[3]);
            bindings.put("G___edgeLabel", e[4]);
            bindings.put("G___date", e[5]);
            bindings.put("G___weight", Double.parseDouble(e[6]));
            List<Result> results = execute(client, ADD_EDGE_DSL, bindings);
            System.out.println("添加边: " + e[0] + " -[" + e[4] + "]-> " + e[2] + " -> " + results);
        }
    }

    // ========== 3. 查询 ==========

    private static void queryVertex(Client client, String vertexId, String label) {
        Map<String, Object> bindings = new HashMap<>();
        bindings.put("G___id", vertexId);
        bindings.put("G___label", label);
        List<Result> results = execute(client, QUERY_VERTEX_DSL, bindings);
        for (Result r : results) {
            System.out.println(r.getObject());
        }
    }

    private static void queryNeighbors(Client client, String vertexId, String label, String edgeLabel) {
        Map<String, Object> bindings = new HashMap<>();
        bindings.put("G___id", vertexId);
        bindings.put("G___label", label);
        bindings.put("G___edgeLabel", edgeLabel);
        List<Result> results = execute(client, QUERY_NEIGHBORS_DSL, bindings);
        for (Result r : results) {
            System.out.println(r.getObject());
        }
    }

    private static void queryVectorSearch(Client client, String label, String prop, int topK) {
        List<Float> queryVector = randomVector(VECTOR_DIM);
        Map<String, Object> bindings = new HashMap<>();
        bindings.put("G___label", label);
        bindings.put("G___prop", prop);
        bindings.put("G___vector", queryVector);
        bindings.put("G___topK", topK);
        List<Result> results = execute(client, VECTOR_SEARCH_DSL, bindings);
        for (Result r : results) {
            System.out.println(r.getObject());
        }
    }

    // ========== 工具方法 ==========

    /** 执行 Gremlin DSL,通过 RequestOptions 传递 bindings 参数。 */
    private static List<Result> execute(Client client, String dsl, Map<String, Object> bindings) {
        RequestOptions.Builder options = RequestOptions.build().timeout(REQUEST_TIMEOUT_MS);
        for (Map.Entry<String, Object> entry : bindings.entrySet()) {
            options.addParameter(entry.getKey(), entry.getValue());
        }
        ResultSet resultSet = client.submit(dsl, options.create());
        return resultSet.all().join();
    }

    /** 生成随机浮点向量。 */
    private static List<Float> randomVector(int dim) {
        List<Float> vec = new ArrayList<>(dim);
        for (int i = 0; i < dim; i++) {
            vec.add(Math.round((RANDOM.nextFloat() * 2 - 1) * 1000000f) / 1000000f);
        }
        return vec;
    }

    /** 发送带 Basic Auth 的 JSON POST 请求。 */
    private static String httpPost(String urlValue, String jsonBody, Config config) throws Exception {
        HttpURLConnection conn = (HttpURLConnection) new URL(urlValue).openConnection();
        conn.setRequestMethod("POST");
        setBasicAuth(conn, config);
        conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        conn.setConnectTimeout(10000);
        conn.setReadTimeout(30000);
        conn.setDoOutput(true);
        try (OutputStream out = conn.getOutputStream()) {
            out.write(jsonBody.getBytes(StandardCharsets.UTF_8));
        }
        int code = conn.getResponseCode();
        InputStream input = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
        if (input == null) {
            return "HTTP " + code;
        }
        StringBuilder body = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                body.append(line);
            }
        }
        return "HTTP " + code + ": " + body;
    }

    /** 设置 HTTP Basic Auth 请求头。 */
    private static void setBasicAuth(HttpURLConnection conn, Config config) {
        conn.setRequestProperty("Authorization", "Basic " + Base64.getEncoder()
                .encodeToString((config.username + ":" + config.password).getBytes(StandardCharsets.UTF_8)));
    }

    /** 从 Gremlin remote.yaml 文件加载连接配置。 */
    private static Config loadConfig(String path) throws Exception {
        Map<?, ?> yaml;
        try (InputStream in = new FileInputStream(path)) {
            yaml = (Map<?, ?>) new Yaml().load(in);
        }
        Config config = new Config();
        config.remoteYamlPath = path;
        if (yaml.get("hosts") != null) {
            Object hosts = yaml.get("hosts");
            config.host = hosts instanceof List && !((List<?>) hosts).isEmpty()
                    ? String.valueOf(((List<?>) hosts).get(0))
                    : String.valueOf(hosts);
        }
        if (yaml.get("port") != null) {
            config.gremlinPort = Integer.parseInt(String.valueOf(yaml.get("port")));
        }
        if (yaml.get("username") != null) {
            config.username = String.valueOf(yaml.get("username"));
        }
        if (yaml.get("password") != null) {
            config.password = String.valueOf(yaml.get("password"));
        }
        if (yaml.get("path") != null) {
            String value = String.valueOf(yaml.get("path"));
            config.dbName = value.substring(value.lastIndexOf('/') + 1);
        }
        return config;
    }

    private static void printSection(String title) {
        String line = new String(new char[50]).replace('\0', '=');
        System.out.println("\n" + line);
        System.out.println(title);
        System.out.println(line);
    }

    /** 运行配置。 */
    private static class Config {
        String remoteYamlPath;
        String host = "127.0.0.1";
        int gremlinPort = 8182;
        String dbName = "default";
        String username = "admin";
        String password = "admin";

        String schemaBaseUrl() {
            return "http://" + host + ":" + gremlinPort;
        }
    }
}

编译并运行

在项目根目录下执行以下命令打包并运行:

mvn clean package -DskipTests
java -jar target/lindorm-graph-java-demo-1.0.0.jar config/remote.yaml