defmodule RedisGraph do @moduledoc """ Query builder library that provides functions to construct Cypher queries to communicate with [RedisGraph](https://redis.io/docs/stack/graph/) database and interact with the entities through defined structures. The library is developed on top of an existing [library](https://github.com/crflynn/redisgraph-ex), written by Christopher Flynn, and provides additional functionality with refactored codebase to support [RedisGraph result set](https://redis.io/docs/stack/graph/design/client_spec/). To run `RedisGraph` locally with Docker, use ```bash docker run -p 6379:6379 -it --rm redis/redis-stack-server ``` Here is a simple example of how to use the library: ```elixir alias RedisGraph.{Query, Graph, QueryResult} # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{ name: "social" }) {:ok, query} = Query.new() |> Query.create() |> Query.node(:n, ["Person"], %{age: 30, name: "John Doe", works: true}) |> Query.relationship_from_to(:r, "TRAVELS_TO", %{purpose: "pleasure"}) |> Query.node(:m, ["Place"], %{name: "Japan"}) |> Query.return(:n) |> Query.return_property(:n, "age", :Age) |> Query.return(:m) |> Query.build_query() # query will hold # "CREATE (n:Person {age: 30, name: 'John Doe', works: true})-[r:TRAVELS_TO {purpose: 'pleasure'}]->(m:Place {name: 'Japan'}) RETURN n, n.age AS Age, m # Execute the query {:ok, query_result} = RedisGraph.query(conn, graph.name, query) # Get result set result_set = Map.get(query_result, :result_set) # result_set will hold # [ # [ # %RedisGraph.Node{ # id: 2, # alias: :n, # labels: ["Person"], # properties: %{age: 30, name: "John Doe", works: true} # }, # 30, # %RedisGraph.Node{ # id: 3, # alias: :m, # labels: ["Place"], # properties: %{name: "Japan"} # } # ] # ] ``` """ alias RedisGraph.{QueryResult, Util} require Logger @type connection() :: GenServer.server() @doc """ Execute arbitrary command against the database. https://redis.io/docs/stack/graph/commands/ Query commands will be a list of strings. They will begin with either `GRAPH.QUERY`, `GRAPH.EXPLAIN`, `GRAPH.DELETE` etc. The next element will be the name of the graph. The third element will be the query command. Optionally pass the last element `--compact` for compact results. Returns a `RedisGraph.QueryResult` containing the result set and metadata associated with the query or error message. ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Create the query query = [ "GRAPH.QUERY", graph.name, "MATCH (a:actor)-[:act]->(m:movie {title:'straight outta compton'}) RETURN a", "--compact" ] # Call the command {:ok, query_result} = RedisGraph.command(conn, query) ``` """ @spec command(connection(), list(String.t())) :: {:ok, QueryResult.t()} | {:error, any()} def command(conn, c) do # Logger.debug(Enum.join(c, " ")) IO.puts("c") IO.inspect(c) case Redix.command(conn, c) do {:ok, result} -> {:ok, QueryResult.new(%{conn: conn, graph_name: Enum.at(c, 1), raw_result_set: result})} {:error, _reason} = error -> error end end @doc """ Run a query on a graph in the database. Returns a `RedisGraph.QueryResult` containing the result set and metadata associated with the query or error message. https://redis.io/commands/graph.query/ ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Create the query query = "MATCH (a:actor)-[:act]->(m:movie {title:'straight outta compton'}) RETURN a" # Call the query {:ok, query_result} = RedisGraph.query(conn, graph.name, query) ``` """ @spec query(connection(), String.t(), String.t()) :: {:ok, QueryResult.t()} | {:error, any()} def query(conn, graph_name, q) do c = ["GRAPH.QUERY", graph_name, q, "--compact"] command(conn, c) end @doc """ Fetch the execution plan for a query on a graph. Returns a raw result containing the query plan. https://redis.io/commands/graph.explain/ ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Create the query query = "MATCH (a:actor)-[:act]->(m:movie {title:'straight outta compton'}) RETURN a" # Call the query {:ok, query_result} = RedisGraph.execution_plan(conn, graph.name, query) ``` """ @spec execution_plan(connection(), String.t(), String.t()) :: {:ok, list()} | {:error, any()} def execution_plan(conn, graph_name, q) do c = ["GRAPH.EXPLAIN", graph_name, q] case Redix.command(conn, c) do {:error, _reason} = error -> error {:ok, result} -> # Logger.debug(result) {:ok, result} end end @doc """ Delete a graph from the database. Returns a `RedisGraph.QueryResult` containing the result set and metadata associated with the query or error message. https://redis.io/commands/graph.delete/ ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Call the query {:ok, query_result} = RedisGraph.delete(conn, graph.name) ``` """ @spec delete(connection(), String.t()) :: {:ok, QueryResult.t()} | {:error, any()} def delete(conn, graph_name) do command = ["GRAPH.DELETE", graph_name] RedisGraph.command(conn, command) end @doc """ Execute a procedure call against the graph specified and receive the raw result of the procedure call. https://redis.io/docs/stack/graph/design/client_spec/#procedure-calls ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Call the query {:ok, query_result} = RedisGraph.call_procedure_raw(conn, graph.name, "db.labels") ``` """ @spec call_procedure_raw(connection(), String.t(), String.t(), list(), map()) :: {:ok, list()} | {:error, any()} def call_procedure_raw(conn, graph_name, procedure, args \\ [], kwargs \\ %{}) do args = Enum.map_join(args, ",", &Util.value_to_string/1) yields = Map.get(kwargs, "y", []) yields = if length(yields) > 0 do " YIELD " <> Enum.join(yields, ",") else "" end q = "CALL " <> procedure <> "(" <> args <> ")" <> yields c = ["GRAPH.QUERY", graph_name, q, "--compact"] case Redix.command(conn, c) do {:ok, result} -> {:ok, result} {:error, _reason} = error -> error end end @doc """ Execute a procedure call against the graph specified. Returns a `RedisGraph.QueryResult` containing the result set and metadata associated with the query or error message. https://redis.io/docs/stack/graph/design/client_spec/#procedure-calls ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Call the query {:ok, query_result} = RedisGraph.call_procedure(conn, graph.name, "db.labels") ``` """ @spec call_procedure(connection(), String.t(), String.t(), list(), map()) :: {:ok, QueryResult.t()} | {:error, any()} def call_procedure(conn, graph_name, procedure, args \\ [], kwargs \\ %{}) do args = Enum.map_join(args, ",", &Util.value_to_string/1) yields = Map.get(kwargs, "y", []) yields = if length(yields) > 0 do " YIELD " <> Enum.join(yields, ",") else "" end q = "CALL " <> procedure <> "(" <> args <> ")" <> yields c = ["GRAPH.QUERY", graph_name, q, "--compact"] RedisGraph.command(conn, c) end @doc """ Fetch response of the `db.labels()` procedure call against the specified graph. It returns a `RedisGraph.QueryResult` containing the result set and metadata from the call. ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Call the query {:ok, query_result} = RedisGraph.labels(conn, graph.name) ``` """ @spec labels(connection(), String.t()) :: {:ok, list()} | {:error, any()} def labels(conn, graph_name) do call_procedure(conn, graph_name, "db.labels") end @doc """ Fetch response of the `db.relationshipTypes()` procedure call against the specified graph. It returns a `RedisGraph.QueryResult` containing the result set and metadata from the call. ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Call the query {:ok, query_result} = RedisGraph.relationship_types(conn, graph.name) ``` """ @spec relationship_types(connection(), String.t()) :: {:ok, list()} | {:error, any()} def relationship_types(conn, graph_name) do call_procedure(conn, graph_name, "db.relationshipTypes") end @doc """ Fetch response of the `db.propertyKeys()` procedure call against the specified graph. It returns a `RedisGraph.QueryResult` containing the result set and metadata from the call. ## Example: ``` alias RedisGraph.Graph # Create a connection using Redix {:ok, conn} = Redix.start_link("redis://localhost:6379") # Create a graph graph = Graph.new(%{name: "imdb"}) # Call the query {:ok, query_result} = RedisGraph.property_keys(conn, graph.name) ``` """ @spec property_keys(connection(), String.t()) :: {:ok, list()} | {:error, any()} def property_keys(conn, graph_name) do call_procedure(conn, graph_name, "db.propertyKeys") end end