Lage.tech

BotmationNotes
moon indicating dark mode
sun indicating light mode

Neo4j

One of my closed-source projects has an API called Webb (after the James Webb Space Telescope) that interfaces a graphing database called Neo4j. It’s the brain 🧠 of the project.

Unlock more value from your data by freely relating it. As different as your models may be, Neo4J makes it easy to associate them by drawing relationships between them using cypher.

When it comes to interfacing in NodeJS, use the neo4j-driver.

The following notes focus on the reactive approach using RxJS and Reactive Sessions.

Transactions

Queries are performed in Transactions.

The neo4j-driver package’s sessions support 3 types of transactions.

  1. Auto-commit
  2. Read
  3. Write

The Read and Write transaction types are preferred, for if there is a transient error within the driver, the driver will automatically retry the transaction, unlike auto-commit.

Auto-commit

Auto-commit transactions are immediately executed against the DBMS and acknowledged immediately. These are done through the run() method on the session object, ie:

// run query in auto-commit transaction
rxSession.run('MATCH (m)-[r]->(n) RETURN m,n,r');

Important! The driver will not reattempt the query upon any errors with an auto-commit transaction. Therefore, it shouldn’t be used in production, but perhaps in one-off queries like seeding initial data.

Read

Use the executeRead() method to provide a single parameter, a transaction callback method, that gives you a transaction object to perform the query via the read transaction’s run() method, ie:

// Get people's names who like the dessert: ice-cream
rxSession.executeRead((tx) =>
tx
.run(
`MATCH (p:Person)-[:LIKES]->(d:Dessert)
WHERE d.name = $name
return p.name as name
LIMIT 10`,
{ name: 'ice-cream' }
)
.records() // returns RxJS Observable
.pipe(
map((record) => record.get('name')),
toArray(), // wait for query completion
catchError((e) => throwError(() => e))
)
);

Write

Use the executeWrite() method to provide a transaction callback method that gives you a transaction object to perform a write query that has built-in cluster support (goes to leader for leader to sync with followers), ie:

// Add Michael as a Person
rxSession.executeWrite((tx) =>
tx
.run(`MERGE (p:Person {name: $name})`, { name: 'Michael' }) // $name is replaced with the corresponding key's value
.records()
.pipe(
last(), // wait for query to complete
map((record) => record.get('name')),
catchError((e) => throwError(() => e))
)
);

Close

It’s important to close a transaction’s session after the transaction’s query has completed:

return concat(
// combine the two observables transaction's records() and session's close()
rxSession.executeRead((txc) =>
txc
.run('MATCH (signal:Signal {url: $url}) RETURN signal LIMIT 1', {
url,
})
.records()
.pipe(
last(), // make sure the query has completed before mapping the result
map((record) => record.get('signal')),
catchError((e) => throwError(() => e))
)
),
rxSession.close()
);

Constraints

For adding constaints, create them via cypher:

CREATE CONSTRAINT personIdConstraint FOR (person:Person) REQUIRE person.id IS UNIQUE

Learn more here.

Indexes for performance

Learn more here.

IF NOT EXISTS makes a query idempotent, therefore no error will be thrown if you attempt to create the same index twice.

Cypher functions

There are functions available within Cypher queries to make it easier to write queries. Predicate functions, Scalar functions ie creating a UUID, and more.

Cypher functions

Deleting all data in the Database

Becareful! This will delete all nodes and relationships in the database that you run this query in, you’ve been warned!!

MATCH (n)
DETACH DELETE n

Helpful when developing, expirmenting with new things, trying out new Models, etc.

Gotcha’s

int()

Use the int() function from neo4j-driver to convert the values in a query into Neo4j integers since the Java Integer type and the Javascript Integer type support different range of number sizes. To get around this, anything too big for Javascript is casted into a string.

import { int } from 'neo4j-driver';
const res = await session.readTransaction((tx) =>
tx.run(
`
MATCH (m:Movie)
SKIP $skip
LIMIT $limit
`,
{ skip: int(skip), limit: int(limit) }
)
);

Native Types

import { toNativeTypes } from 'neo4j-driver';
const movies = res.records.map((row) => toNativeTypes(row.get('movie')));