Manipulate query results

In the basic query workflows examples, transaction functions never returned a query result directly. Instead, they always manipulated it in some way: either casting it to list, or calling a specific method. This is because the driver’s output of a query is a Result object, which does not directly contain the result records. Rather, it encapsulates the Cypher result in a rich data structure that requires some parsing within the transaction function.

This section shows how to work with a Result object so as to extract data in the form that is most convenient for your application.

Manipulate the result

When running a query, the result records are not immediately and entirely fetched and returned by the server. Instead, results come as a lazy stream. In particular, when the driver receives some records from the server, they are initially buffered in a background queue. Records stay in the buffer until they are consumed by the application, at which point they are removed from the buffer. There is thus no way to retrieve a previous record from the stream, unless manually saved in an auxiliary data structure. When no more records are available, the result is exhausted.

It is up to the transaction function to do all the processing. To process a result, you have two main options:

  1. cast the result object to list, which yields a list of Record objects. Use the .data() method on a Record to obtain a dict-like view, or the .get(key, default=None) method to retrieve the value for a given key.

    Processing result by casting it to list or with list comprehension
    def match_person_nodes(tx, name_filter):
        result = tx.run("""
            MATCH (p:Person) WHERE p.name STARTS WITH $filter
            RETURN p.name as name ORDER BY name
            """, filter=name_filter)
        return list(result)
        # or: return [record["name"] for record in result]
    
    with driver.session(database="neo4j") as session:
        people = session.execute_read(match_person_nodes, "Al")
    
        for person in people:
            print(person.data())  # or person.get("name")
  2. use any of the of the Result methods. The ones most commonly needed are listed in the table below.

    Table 1. An essential list of Result methods.
    Name Description

    value(key=0, default=None)

    Return the remainder of the result as a list. If key is specified, only the given property is included, and default allows to specify a value for nodes lacking that property.

    fetch(n)

    Return up to n records from the result.

    single(strict=False)

    Return the next and only remaining record, or None. Calling this method always exhausts the result.

    If more than one record is available,

    - strict==False — a warning is generated and the first of these is returned; - strict==True — a ResultNotSingleError is raised.

    peek()

    Return the next record from the result without consuming it. This leaves the record in the buffer for further processing.

    data(*keys)

    Return a JSON-like dump of the raw result. Only use it for debugging purposes.

    consume()

    Return the query ResultSummary. It exhausts the result, so should only be called when data processing is over.

    See the API documentation for a complete list of Result methods.

A transaction function must not return the neo4j.Result itself. Doing so is roughly equivalent to returning a pointer to the result buffer, which gets invalidated as soon as the query’s transaction is over.

Examples

Processing result with single and consume.
def get_single_person(tx, name):
    """Get a single record (or an exception) and the summary from a result."""

    result = tx.run("MATCH (a:Person {name: $name}) RETURN a.name AS name",
                    name=name)
    record = result.single(strict=True)
    summary = result.consume()
    return record, summary


with driver.session(database="neo4j") as session:
    record, summary = session.execute_read(
        get_single_person, name="Alice"
    )
    print(record)
Processing result with fetch and peek.
def get_exactly_5_people(tx):
    result = tx.run("MATCH (a:Person) RETURN a.name AS name")
    records = result.fetch(5)

    if len(records) != 5:
        raise Exception(f"Expected exactly 5 records, found only {len(records)}")
    if result.peek():
        raise Exception("Expected exactly 5 records, found more")

    return records


with driver.session(database="neo4j") as session:
    records = session.execute_read(get_exactly_5_people)
    print(records)

Transform to pandas DataFrame

The method .to_df() converts the result into a pandas DataFrame. This method is only available if the pandas library is installed.

Return a DataFrame with two columns (n and m) and 10 rows
def pandas_df(tx):
    result = tx.run("UNWIND range(1, 10) AS n RETURN n, n+1 AS m")
    return result.to_df()

with driver.session(database="neo4j") as session:
    df = session.execute_read(pandas_df)

This method accepts two optional arguments:

  • expand — If True, some data structures in the result are recursively expanded and flattened. More info in the API documentation.

  • parse_dates — If True, columns exclusively containing time.DateTime objects, time.Date objects, or None, are converted to pandas.Timestamp.

Transform to graph

The method .graph() converts the result into a graph. To make the most out of this method, your query should return a graph-like result instead of a single column. The resulting graph object exposes the properties nodes and relationships, which are set views into Node and Relationship objects.

You can use the graph format for further processing or to visualize the query result. An example implementation that uses the pyvis library to draw the graph is below.

Visualize graph result with pyvis.
import pyvis
from neo4j import GraphDatabase
import neo4j


def visualize_result(query_graph, nodes_text_properties):
    visual_graph = pyvis.network.Network()

    for node in query_graph.nodes:
        node_label = list(node.labels)[0]
        node_text = node[nodes_text_properties[node_label]]
        visual_graph.add_node(node.element_id, node_text, group=node_label)

    for relationship in query_graph.relationships:
        visual_graph.add_edge(relationship.start_node.element_id,
                              relationship.end_node.element_id,
                              title=relationship.type)

    visual_graph.show('network.html')


def main():
    URI = "neo4j://localhost:7687"
    AUTH = ("neo4j", "secretgraph")

    with GraphDatabase.driver(URI, auth=AUTH) as driver:

        friends_list = [("Arthur", "Guinevre"),
                        ("Arthur", "Lancelot"),
                        ("Arthur", "Merlin")]

        with driver.session(database="neo4j") as session:
            for pair in friends_list:
                session.execute_write(create_friends, pair[0], pair[1])
            session.execute_write(create_film, "Wall-E")
            session.execute_write(like_film, "Wall-E", "Arthur")
            graph = session.execute_read(get_person_graph, "Arthur")

        # Draw graph
        nodes_text_properties = {  # what property to use as text for each node
            "Person": "name",
            "Film": "title",
        }
        visualize_result(graph, nodes_text_properties)


def create_friends(tx, name, friend_name):
    tx.run("""
        MERGE (a:Person {name: $name})
        MERGE (a)-[:KNOWS]->(friend:Person {name: $friend_name})
        """, name=name, friend_name=friend_name
    )


def create_film(tx, title):
    tx.run("MERGE (film:Film {title: $title})", title=title)


def like_film(tx, title, person_name):
    tx.run("""
        MATCH (film:Film {title: $title})
        MATCH (liker:Person {name: $person_name})
        MERGE (liker)-[:LIKES]->(film)
        """, title=title, person_name=person_name,
    )


def get_person_graph(tx, name):
    result = tx.run("""
        MATCH (a:Person {name: $name})-[r]-(b)
        RETURN a,r,b
        """, name=name
    )
    return result.graph()


if __name__ == "__main__":
    main()
pyvis example
Figure 1. Graph visualization of example above

Glossary

LTS

A Long Term Support release is one guaranteed to be supported for a number of years. Neo4j 4.4 is LTS, and Neo4j 5 will also have an LTS version.

Aura

Aura is Neo4j’s fully managed cloud service. It comes with both free and paid plans.

Driver

A Driver object holds the details required to establish connections with a Neo4j database. Every Neo4j-backed application requires a Driver object.

Cypher

Cypher is Neo4j’s graph query language that lets you retrieve data from the graph. It is like SQL, but for graphs.

APOC

Awesome Procedures On Cypher (APOC) is a library of (many) functions that can not be easily expressed in Cypher itself.

Bolt

Bolt is the protocol used for interaction between Neo4j instances and drivers. It listens on port 7687 by default.

ACID

Atomicity, Consistency, Isolation, Durability (ACID) are properties guaranteeing that database transactions are processed reliably. An ACID-compliant DBMS ensures that the data in the database remains accurate and consistent despite failures.

eventual consistency

A database is eventually consistent if it provides the guarantee that all cluster members will, at some point in time, store the latest version of the data.

causal consistency

A database is causally consistent if read and write queries are seen by every member of the cluster in the same order. This is stronger than eventual consistency.

null

The null marker is not a type but a placeholder for absence of value. For more information, see Cypher Manual — Working with null.

transaction

A transaction is a unit of work that is either committed in its entirety or rolled back on failure. An example is a bank transfer: it involves multiple steps, but they must all succeed or be reverted, to avoid money being subtracted from one account but not added to the other.