Neo4j – H3 Library – Update

After about four years (where did that time go?), I have circled back to Neo4j and H3 geospatial data processing.

Since version 3.4, Neo4j has a native geospatial datatype. Neo4j uses the WGS-84 and WGS-84 3D coordinate reference system. Within Neo4j, we can index these point properties and query using our distance function or you query within a bounding box.

In the next few posts, we’ll discuss the new plugin and walk through some examples of how to use the plugin within Neo4j for geospatial usecases.

What is H3

From the website, “H3 is a geospatial indexing system that partitions the world into hexagonal cells. H3 is open source under the Apache 2 license.

The H3 Core Library implements the H3 grid system. It includes functions for converting from latitude and longitude coordinates to the containing H3 cell, finding the center of H3 cells, finding the boundary geometry of H3 cells, finding neighbors of H3 cells, and more.”

Why H3

H3 leverages the power of hexagons to create a global grid system with 16 resolutions, from 0 (the size of a small continent) to 15 (less than a square meter). While the H3 library itself doesn’t perform geospatial analysis, it provides the core building blocks for a range of analytical functions. One of the most common uses is to bin data such as places or events that occur with a specific location. Once the data is binned, we can then compare the count or sum of occurrences in that hex location with data in other bins at the same resolution.

The H3 library helps us make sense of large sets of data. It allows the user to efficiently work with the data through optimized functions such as indexing points to cells, traversing through the hexes and by effectively moving through a hierarchy of hex cells. These functions fit well with Neo4j due to the relationships among the cells.

H3 cell IDs are also perfect for joining disparate datasets. That is, you can perform a spatial join easily without complex geospatial functions. It is straightforward to join datasets by cell ID and start answering location-driven questions. In this blog series, we are going to join Points of Interest data with NYC Neighborhood definitions with NYC Taxi Pickup Zones and with the NYC Taxi dataset.

Databricks and H3

You might be wondering why we are talking about Databricks in this blogpost. On September 14, 2022, Databricks announced built-in H3 expressions for “efficient geospatial processing and analytics”. They announced 28 built-in functions for working with geospatial data. I had recently seen that posting and decided to adopt the Databricks naming convention for the Neo4j plugin.

The Library

The Neo4jH3 library is available here. Besides the for installation instructions, the Documentation is a great resource for the different functions and procedures. I’ve tried my best to provide sufficient examples along with error codes for the functions and procedures.

For this blog series, we will use the 5.3.0 release as it has the new function/procedure naming and works with Neo4j 5.

If you have questions at any point in this blog series, leave me a comment and I’ll be glad to get back to you.

Neo4j – Kafka – JAAS

A couple of weeks ago, I was asked how to configure Neo4j to use JAAS with Kafka using SASL_Plaintext. While the Neo4j documentation does talk about SSL configuration, it doesn’t specifically discuss JAAS.

On the Kafka side, I used a Bitnami AMI (Kafka – AMI ID bitnami-kafka-2.3.0-0-linux-debian-9-x86_64-hvm-ebs-nami (ami-0ca61ab6a3b990db7)) running on AWS. There were some configuration changes I needed to make to enable my local Neo4j instance to connect.

Edit and set the bootstrap.servers property to the public ip address.

On the file, I edited it as follows:

############################# Socket Server Settings #############################

# The address the socket server listens on. It will get the value returned from
# if not configured.

#     listeners = PLAINTEXT://

# Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured.  Otherwise, it will use the value
# returned from


On the Neo4j side, I copied the contents of /home/bitnami/stack/kafka/conf/kafka_jaas.conf and saved it to a file called kafka_client_jaas.conf in the /conf directory on my Neo4j server.

In the neo4j.conf file, I edited it as follows:*, streams.**, streams.*


After restarting Neo4j, I was able to write to the topic on my Kafka cluster by running:

CALL streams.publish('numtest', 'Hello World2 from Neo4j!')

Good luck using Neo4j and Kafka. Drop me a comment if you have any questions.

Neo4j Python 4 Driver – Example

While Neo4j 4.0 was released in December 2019, the Neo4j 4.0 python driver wasn’t ready to be released at that time. In the last week or so, Neo4j released a 4.0.0a4 version of the driver that allows for multi-database support. The driver documentation has been updated to show how to use the latest driver.

Someone asked recently for some sample code on how to use the driver. I created a sample gist showing SSL certificate support, reading a single result, reading multiple results and running create statements in a transaction block.

Give the driver a test drive and let us know if you run into any issues.

Kafka – Neo4j – SSL Config

This blogpost provides guidance to configure SSL security between Kafka and Neo4j. This will provide data encryption between Kafka and Neo4j. This does not address ACL confguration inside of KAFKA.

Self Signed Certificates

This section came from
Make sure that you have truststore and keystore JKSs for each server.+ In case you want a self signed certificate, you can use the following commands:

mkdir security
cd security

export PASSWORD=password
keytool -keystore kafka.server.keystore.jks -alias localhost -validity 365 -genkey
openssl req -new -x509 -keyout ca-key -out ca-cert -days 365
keytool -keystore kafka.server.truststore.jks -alias CARoot -import -file ca-cert
keytool -keystore kafka.client1.truststore.jks -alias CARoot -import -file ca-cert
keytool -keystore kafka.server.keystore.jks -alias localhost -certreq -file cert-file
openssl x509 -req -CA ca-cert -CAkey ca-key -in cert-file -out cert-signed -days 365 -CAcreateserial -passin pass:$PASSWORD
keytool -keystore kafka.server.keystore.jks -alias CARoot -import -file ca-cert
keytool -keystore kafka.server.keystore.jks -alias localhost -import -file cert-signed
keytool -keystore kafka.client1.keystore.jks -alias localhost -validity 365 -genkey
keytool -keystore kafka.client1.keystore.jks -alias localhost -certreq -file cert-file
openssl x509 -req -CA ca-cert -CAkey ca-key -in cert-file -out cert-signed -days 365 -CAcreateserial -passin pass:$PASSWORD
keytool -keystore kafka.client1.keystore.jks -alias CARoot -import -file ca-cert
keytool -keystore kafka.client1.keystore.jks -alias localhost -import -file cert-signed

Once the keystores are created, you have to move the kafka.client1.keystore.jks and kafka.client1.truststore.jks to your neo4j server.

Note: This article discusses addressing this error (Caused by: No subject alternative names present) that may appear when querying the topic. 

Kafka Configuration

Connect to your Kafka server and modify the config/ file.
This configuration worked for me but I have seen other configurations without the EXTERNAL and INTERNAL settings.
This configuration is for Kafka on AWS but should work for other configurations.




Neo4j Configuration

The following is required for a Neo4j configuration. In this case, we are connecting to the public AWS IP address. The keystore and truststore locations point to the files that you created earlier in the steps.

Note: The passwords are stored in plaintext so limit access to this neo4j.conf file.



This line is optional but does help for debugging SSL issues.


After starting Kafka and Neo4j, you can test by creating a Person node in Neo4j and then query the topic as follows: 

 ./bin/ --bootstrap-server localhost:9092 --topic neoTest --from-beginning 

If you want to test using SSL, you would do the following:

1. Create a file consisting of:

2. Query the topic:

./bin/ –bootstrap-server –topic neoTest –consumer.config /home/kafka/ –from-beginning

Spark to Neo4j

This blog post is mostly for my benefit and for the ability to go back and remember work that I have done. With that being said, this blog post talks about the Neo4j Spark Connector that allows a user to write from a Spark Data Frame to a Neo4j database. I will show the configuration settings within the Databricks Spark cluster, build some DataFrames within the Spark notebook and then write nodes to Neo4j and merge nodes to Neo4j.

For this blog post, I am using the community Databricks site to run a simple Spark cluster.


For the cluster, I chose the Databricks 5.4 Runtime version which includes Apache Spark 2.4.3 and Scala 2.11. There are some Spark configuration options that configure the Neo4j user, Neo4j password and the Neo4j url for the bolt protocol.

Under the Libraries, I loaded the Neo4j Spark Connector library.

Once the configurations were updated and the library added, we can restart the cluster.

As a side note, the Notebook capability is a great feature.

Creating Nodes:

First I loaded up the Chicago crime data into a dataframe. Second was to convert the dataframe into a table. From the table, we’ll run some SQL and load the results into Neo4j. These steps are shown below.

Loading CSV into DataFrame
Creating a Table

This next screen shows how we can get a spark context and connect to Neo4j. Once we have the context, we can execute several Cypher queries. For our example, we are going to delete existing data and creating a constraint and an index.

Connecting to Neo4j and Running Queries

In this step, we can select data from the dataframe and load that data into Neo4j using the Neo4jDataFrame.createNodes option.

Loading Crime Nodes into Neo4j
Merging Crime Beat Nodes
Loading Data into Neo4j Graph Algorithm
Executing a Graph Algorithm

As you see, we can move data from Spark to Neo4j in a pretty seamless manner. The entire Spark notebook can be found here.

In the next post, we will connect Spark to Kafka and then allow Neo4j to read from the Kafka topic.

Neo4j 4.0 Docker

With the upcoming Neo4j 4.0 release, it was time to revisit deploying a Neo4j causal cluster via Docker. Our fantastic parter, GraphAware published a quickstart for deploying a cluster in Docker.

Docker is a tool designed to make it easy to create, deploy, and run applications by using containers. Containers allow developers to package an application with all of the components it needs and distribute it as an atomic, universal unit that can run on any host platform.

This docker-compose.yml file will start a three-core causal cluster.

  1. Download the docker-compose.yml
  2. Open a command shell in the same directory and execute:

docker-compose up

That’s it! After allowing each instance to come to life and to discover each other, the cluster is up and running.

Note: There are still some warnings that show up in the logs but these are items that are being worked on.

Neo4j 4.0 changes:

Some things that have changed and require your attention are using the advertised_address instead of listen_address for the clustering protocols.

  • NEO4J_causal__clustering_discovery__advertised__address=core2:5000
  • NEO4J_causal__clustering_transaction__advertised__address=core2:6000
  • NEO4J_causal__clustering_raft__advertised__address=core2:7000

When finished, the cluster can be shut down by opening a shell in the same directory as docker-compose.yml and executing:

docker-compose down

Neo4j – Kafka – MySQL: Configuration – Part 2

In Part 1, we configured Neo4j, Kafka and MySQL to talk using the Neo4j Kafka plugin and Maxwell’s Daemon. In part 2, I will show how data can be added into MySQL and then added/modified/deleted in Neo4j through the Kafka connector.

For our dataset, I chose the Musicbrainz dataset. This dataset has several tables and has a good amount of data to test with. For this test, I am only going to use the Label and the Artists tables but you could easily add more tables and more data.

For the Label table, here is the MySQL schema.

CREATE TABLE `label` (
  `id` bigint(20) DEFAULT NULL,
  `gid` varchar(100) DEFAULT NULL,
  `name` varchar(200) DEFAULT NULL,
  `sort_name` varchar(200) DEFAULT NULL,
  `type` int(11) DEFAULT NULL

When we start Maxwell’s Daemon, it automatically creates two topics on our Kafka machine. One is musicbrainz_musicbrainz_artist and the other is musicbrainz_musicbrainz_label.

When we do a CRUD operation on the MySQL table, the maxwell-daemon will write the operation type to the Kafka topic. For example, here is what an insert into the Artist table looks like:

{"database":"musicbrainz","table":"artist","type":"insert","ts":1563192678,"xid":13695,"xoffset":88,"data":{"gid":"2b99cd8e-55de-4a42-9cb8-6489f3195a4b","name":"Banda Black Rio","sort_name":"Banda Black Rio","id":106851}}
{"database":"musicbrainz","table":"artist","type":"insert","ts":1563192678,"xid":13695,"xoffset":89,"data":{"gid":"38927bad-687f-4189-8dcf-edf1b8f716b4","name":"Kauri Kallas","sort_name":"Kallas, Kauri","id":883445}}

The Neo4j cypher statement in our neo4j.conf file will read from that topic, determine the operation (insert, update or delete) and modify data in Neo4j appropriately.

streams.sink.topic.cypher.musicbrainz_musicbrainz_artist=FOREACH(ignoreMe IN CASE WHEN event.type='insert' THEN [1] ELSE [] END | MERGE (u:Artist{}) on match set =,, on create set =,, FOREACH(ignoreMe IN CASE WHEN event.type='delete' THEN [1] ELSE [] END | MERGE (u:Artist{})  detach delete u) FOREACH(ignoreMe IN CASE WHEN event.type='update' THEN [1] ELSE [] END | MERGE (u:Artist{})  set =,,

To show how this works, we will remove a Label and then re-add that same label. In our Neo4j database, we already have 163K labels. We will delete the XTOY label from MySQL and watch the delete get placed on the Kafka queue and then removed from Neo4j.

In Neo4j, here is the XTOY label.

In MySQL, we are going to run:
delete from label where name=’XTOY’

In our Kafka musicbrainz_musicbrainz_label topic, we see a delete statement coming from MySQL:


Neo4j polls the topic, evaluates the type of action and acts appropriately. Int his case, it should delete the XTOY label. Let’s look at Neo4j now and see if the XTOY label has been removed:

We see that it has been removed. Now, let’s reinsert the record into MySQL and see if Neo4j picks up the insert.

INSERT INTO label(id,gid,name, sort_name, type) values(27894,'e8c1f93b-f518-43f2-b9be-e00e31a5e92d','XTOY','XTOY',-1)


Checking Neo4j once more we see the node has been added in.

The data is automatically replicated across the Neo4j cluster so we don’t have to worry about that aspect.

In summary, it is straight-forward to sync database changes from MySQL to Neo4j through Kafka.

Neo4j – Kafka – MySQL: Configuration – Part 1

With the new Neo4j Kafka streams now available, there has been a few articles such as A New Neo4j Integration with Apache Kafka and How to leverage Neo4j Streams and build a just-in-time data warehouse and Processing Neo4j Transaction Events with KSQL and Kafka Streams and finally How to embrace event-driven graph analytics using Neo4j and Apache Kafka. In this post, I will discuss configuring a Neo4j cluster that will use the Neo4j Kafka Streams to connect to a Kafka server. I will also talk about configuring Maxwell’s Daemon to stream data from MySQL to Kafka and then on to Neo4j.

The new Neo4j Kafka streams library is a Neo4j plugin that you can add to each of your Neo4j instances. It enables three types of Apache Kafka mechanisms:

  • Producer: based on the topics set up in the Neo4j configuration file. Outputs to said topics will happen when specified node or relationship types change
  • Consumer: based on the topics set up in the Neo4j configuration file. When events for said topics are picked up, the specified Cypher query for each topic will be executed
  • Procedure: a direct call in Cypher to publish a given payload to a specified topic

You can get a more detailed overview of how each of these might look like here.


For Kafka, I configured an AWS EC2 instance to serve as my Kafka machine. For the setup, I followed the instructions from the quick start guide up until step 2. Before we get Kafka up and running, we will need to set up the consumer elements in the Neo4j configuration files.

If you are using the Dead Letter Queue functionality in the Neo4j Kafka connector, you will have to create that topic. For the MySQL sync topics, the Maxwell Daemon will automatically create those based on the settings in the file.


For MySQL,  I set up a simple MySQL server on Ubuntu. A couple of things to note.

  • I had to modify the /etc/mysql/my.cnf file and add:
  • I had to create a Maxell user.
CREATE USER 'maxwell'@'%' IDENTIFIED BY 'YourStrongPassword';
	GRANT ALL ON maxwell.* TO 'maxwell'@'%';

Maxwell’s Daemon

For the sync between MySQL and Kafka, I used Maxwell’s daemon, an application that reads MySQL binlogs and writes row updates as JSON to Kafka, Kinesis, or other streaming platforms. Maxwell has low operational overhead, requiring nothing but mysql and a place to write to. Its common use cases include ETL, cache building/expiring, metrics collection, search indexing and inter-service communication. Maxwell gives you some of the benefits of event sourcing without having to re-architect your entire platform.

I downloaded Maxwell’s Daemon and installed it on the MySQL server. I then made the following configuration changes in the file.

# mysql login info

By configuring the kafka_topic to be linked to the database and the table, Maxwell automatically creates a topic for each table in the database.

Neo4j Cluster

Configuring a Neo4j cluster is covered in the Neo4j Operations Manual

We will use the Neo4j Streams plugin. As the instructions say, we download the latest release jar from latest and copy it into $NEO4J_HOME/plugins on each of the Neo4j cluster members. Then we will need to do some configuration.

In the Neo4j.conf file, we will need to configure the Neo4j Kafka plugin. This configuration will be the same for all Core servers in the Neo4j cluster. The neo4j.conf configuration is as follows:

### Neo4j.conf

streams.sink.topic.cypher.Neo4jPersonTest=MERGE (p:Person{name:, surname: event.surname}) MERGE (f:Family{name: event.surname}) MERGE (p)-[:BELONGS_TO]->(f)

streams.sink.topic.cypher.musicbrainz_musicbrainz_artist=FOREACH(ignoreMe IN CASE WHEN event.type='insert' THEN [1] ELSE [] END | MERGE (u:Artist{}) on match set =,, on create set =,, FOREACH(ignoreMe IN CASE WHEN event.type='delete' THEN [1] ELSE [] END | MERGE (u:Artist{})  detach delete u) FOREACH(ignoreMe IN CASE WHEN event.type='update' THEN [1] ELSE [] END | MERGE (u:Artist{})  set =,,

streams.sink.topic.cypher.musicbrainz_musicbrainz_label=FOREACH(ignoreMe IN CASE WHEN event.type='insert' THEN [1] ELSE [] END | MERGE (u:Label{}) on match set =,,, on create set =,,, FOREACH(ignoreMe IN CASE WHEN event.type='delete' THEN [1] ELSE [] END | MERGE (u:Label{})  detach delete u) FOREACH(ignoreMe IN CASE WHEN event.type='update' THEN [1] ELSE [] END | MERGE (u:Label{})  set =,,,


The Neo4j kafka plug-in will poll to see who the Neo4j cluster leader is. The Neo4j cluster leader will automatically poll the Kafka topics for the data changes. If the cluster leader switches, the new leader will take over the polling and the retrieval from the topics.

If you want to change the number of records that Neo4j pulls from the Kafka topic, you can add this setting to your neo4j.conf file:


Once all of the configuration is completed, I have a running MySQL instance, a Kafka instance, a Maxwell’s Daemon configured to read from MySQL and write to Kafka topics and a Neo4j cluster that will read from the Kafka topics.

In part 2, we will show how this all works together.

Neo4j – Finding a doctor on the way to find coffee

I love coffee. I love finding new coffee shops and trying out great coffee roasters. Some of my favorites are Great Lakes Coffee, Stumptown Roasters in NYC’s Ace Hotel, Red Rooster (my choice for home delivery) and my go-to local coffee shop, The Grounds. The Grounds is owned by friends and you have to love their hashtag #LoveCoffeeLovePeople.

You are probably asking what this has to do with Neo4j or anything in general. Go pour yourself a cup of coffee and then come back to see where this leads.

In the Neo4j Uber H3 blog post, you saw how we could use the hexaddress to find doctors within a radius, bounding box, polygon search or even along a line between locations. The line between locations feature got me thinking what if I could get use that feature and combine it with turn-by-turn directions to find a doctor along the route. You never know when you may have had too much coffee and need to find the closest doctor. Or maybe you have a lot of event data (IED explosions for example) and you want to see which ones have occurred along a proposed route.

Let’s see if we can pull this off. I remembered that I had written some python code in conjunction with the Google Directions API. The directions API takes in a start address and an end address. For example:'1700 Courthouse Rd, Stafford, VA 22554'&destination='50 N. Stafford Complex, Center St Suite 107, Stafford, VA 22556'&key='yourapikey'

The API returns a JSON document which includes routes and legs. Each leg has a start_location with a lat and lng value and an end_location with a lat and lng value. Check the link for details on the JSON format.

In my python code, I make a call to the Directions API. I then parse the routes and associated legs (that just sounds weird) to get the start lat/lng and the end lat/lng for each leg. I can then pass the pair into my Neo4j procedure (com.dfauth.h3.lineBetweenLocations), get the results and output the providers that are along that line. Here’s an example:

neoQuery = "CALL com.dfauth.h3.lineBetweenLocations(" + str(prevLat) + "," + str(prevLng) + "," + str(endlat) + "," +  str(endlng) +") yield nodes unwind nodes as locNode match (locNode)<-[:HAS_PRACTICE_AT]-(p:Provider)-[:HAS_SPECIALTY]->(t:TaxonomyCode) return distinct locNode.Address1 + ' ' + locNode.CityName +', ' + locNode.StateName as locationAddress, locNode.latitude as latitude, locNode.longitude as longitude, coalesce(p.BusinessName,p.FirstName + ' ' + p.LastName) as practiceName, p.NPI as NPI order by locationAddress;"
#	    		print(neoQuery)
	    		result =
	    		for record in result:
	    			print(record["locationAddress"] + " " + record["practiceName"])

When I tried this from my house to The Grounds, I got these results:

On the 15.5 minute drive, I would pass about 10 doctors. Now the NPI data isn’t totally accurate but you can see what is available along the route.

I thought it was cool. My python code (minus the API keys) are on Github. Refill your coffee and thanks for reading.

Neo4j – Uber H3 – Geospatial

We are going to take a slight detour with regards to the healthcare blog series and talk about Uber H3. H3 is a hexagonal hierarchical geospatial indexing system. It comes with an API for indexing coordinates into a global grid. The grid is fully global and you can choose your resolution. The advantages and disadvantages of the hexagonal grid system are discussed here and here . Uber open-sourced the H3 indexing system and it comes with a set of java bindings that we will use with Neo4j.

Since version 3.4, Neo4j has a native geospatial datatype. Neo4j uses the WGS-84 and WGS-84 3D coordinate reference system. Within Neo4j, we can index these point properties and query using our distance function or you query within a bounding box. Max DeMarzi blogged about this as did Neo4j. At GraphConnect 2018, Will Lyon and Craig Taverner had a session on Neo4j and Going Spatial. These are all great resources for Neo4j and geo-spatial search.

Why Uber H3

Uber H3 adds some new features that can be extended through Neo4j procedures. Specifically, we want to be able to query based on a polygon, query based on a polygon with holes, and even along a line. We will use the data from our healthcare demo dataset and show how we can use H3 hexagon addresses to help our queries.

H3 Procedure

For our first procedure, we will pass in the latitude, longitude and resolution and receive a hexAddress back.

The H3 interface also allows us to query via a polygon. With Neo4j, we can query using a bounding box like so:

Neo4j Bounding Box Query

In H3, the polygon search looks like this:

This returns in 17ms against 4.4 million locations.

If we combine this with our healthcare data, we can find providers with a certain taxonomy.

We can make this polygon as complicated as we need. This example is for a county polygon:

CALL com.dfauth.h3.polygonSearch([{lon:"-77.302457",lat:"38.504683"},{lon:"-77.310334",lat:"38.493926"},{lon:"-77.322622",lat:"38.467131"},{lon:"-77.32544",lat:"38.44885"},{lon:"-77.319036",lat:"38.417803"},{lon:"-77.310719",lat:"38.397669"},{lon:"-77.312201",lat:"38.390958"},{lon:"-77.314848",lat:"38.389579"},{lon:"-77.317288",lat:"38.383576"},{lon:"-77.296077",lat:"38.369797"},{lon:"-77.288145",lat:"38.359477"},{lon:"-77.28835",lat:"38.351286"},{lon:"-77.286202",lat:"38.347025"},{lon:"-77.286202",lat:"38.347024"},{lon:"-77.321403",lat:"38.345226"},{lon:"-77.339268",lat:"38.302723"},{lon:"-77.345728",lat:"38.26139"},{lon:"-77.326692",lat:"38.245136"},{lon:"-77.370301",lat:"38.246576"},{lon:"-77.39085",lat:"38.245589"},{lon:"-77.420148",lat:"38.257986"},{lon:"-77.447126",lat:"38.284614"},{lon:"-77.455692",lat:"38.301341"},{lon:"-77.467053",lat:"38.31866"},{lon:"-77.475137",lat:"38.32096"},{lon:"-77.478996",lat:"38.316693"},{lon:"-77.498754",lat:"38.32543"},{lon:"-77.506782",lat:"38.325925"},{lon:"-77.527185",lat:"38.320655"},{lon:"-77.526243",lat:"38.309531"},{lon:"-77.530289",lat:"38.309172"},{lon:"-77.54546",lat:"38.325081"},{lon:"-77.584673",lat:"38.346806"},{lon:"-77.594796",lat:"38.336022"},{lon:"-77.618727",lat:"38.367835"},{lon:"-77.634835",lat:"38.409713"},{lon:"-77.628433",lat:"38.452075"},{lon:"-77.634157",lat:"38.464508"},{lon:"-77.568349",lat:"38.520177"},{lon:"-77.530914",lat:"38.555929"},{lon:"-77.481488",lat:"38.592432"},{lon:"-77.463949",lat:"38.578686"},{lon:"-77.448683",lat:"38.580792"},{lon:"-77.395824",lat:"38.545827"},{lon:"-77.370142",lat:"38.519865"},{lon:"-77.334902",lat:"38.514569"},{lon:"-77.308138",lat:"38.499699"},{lon:"-77.310528",lat:"38.505289"},{lon:"-77.302457",lat:"38.504683"}],[{}]) yield nodes return nodes

In these examples, there are no “donut holes”. If I need a polygon donut hole to exclude an area, I can pass it in as the second parameter.

CALL com.dfauth.h3.polygonSearch([{lon:"-77.302457",lat:"38.504683"},{lon:"-77.310334",lat:"38.493926"},{lon:"-77.322622",lat:"38.467131"},{lon:"-77.32544",lat:"38.44885"},{lon:"-77.319036",lat:"38.417803"},{lon:"-77.310719",lat:"38.397669"},{lon:"-77.312201",lat:"38.390958"},{lon:"-77.314848",lat:"38.389579"},{lon:"-77.317288",lat:"38.383576"},{lon:"-77.296077",lat:"38.369797"},{lon:"-77.288145",lat:"38.359477"},{lon:"-77.28835",lat:"38.351286"},{lon:"-77.286202",lat:"38.347025"},{lon:"-77.286202",lat:"38.347024"},{lon:"-77.321403",lat:"38.345226"},{lon:"-77.339268",lat:"38.302723"},{lon:"-77.345728",lat:"38.26139"},{lon:"-77.326692",lat:"38.245136"},{lon:"-77.370301",lat:"38.246576"},{lon:"-77.39085",lat:"38.245589"},{lon:"-77.420148",lat:"38.257986"},{lon:"-77.447126",lat:"38.284614"},{lon:"-77.455692",lat:"38.301341"},{lon:"-77.467053",lat:"38.31866"},{lon:"-77.475137",lat:"38.32096"},{lon:"-77.478996",lat:"38.316693"},{lon:"-77.498754",lat:"38.32543"},{lon:"-77.506782",lat:"38.325925"},{lon:"-77.527185",lat:"38.320655"},{lon:"-77.526243",lat:"38.309531"},{lon:"-77.530289",lat:"38.309172"},{lon:"-77.54546",lat:"38.325081"},{lon:"-77.584673",lat:"38.346806"},{lon:"-77.594796",lat:"38.336022"},{lon:"-77.618727",lat:"38.367835"},{lon:"-77.634835",lat:"38.409713"},{lon:"-77.628433",lat:"38.452075"},{lon:"-77.634157",lat:"38.464508"},{lon:"-77.568349",lat:"38.520177"},{lon:"-77.530914",lat:"38.555929"},{lon:"-77.481488",lat:"38.592432"},{lon:"-77.463949",lat:"38.578686"},{lon:"-77.448683",lat:"38.580792"},{lon:"-77.395824",lat:"38.545827"},{lon:"-77.370142",lat:"38.519865"},{lon:"-77.334902",lat:"38.514569"},{lon:"-77.308138",lat:"38.499699"},{lon:"-77.310528",lat:"38.505289"},{lon:"-77.302457",lat:"38.504683"}],[{lon:'-77.530886',lat:'38.3609911'},{lon:'-77.524492',lat:'38.3411698'},{lon:'-77.524492',lat:'38.277433'},{lon:'-77.5731773',lat:'38.2774607'},{lon:'-77.594635',lat:'38.2771873'}]) yield nodes return nodes

In the 3.3.0 release of the Java bindings, H3 included the ability to return all hex addresses along a line between two points. One could combine this with a service like Google Directions to find all locations along a route. Imagine finding doctors along a route or find all events that occurred along a route.
Here is an example that returns providers who have billing addresses along a line:

CALL com.dfauth.h3.lineBetweenLocations(38.418582, -77.385268,38.500603, -77.444288) yield nodes 
unwind nodes as locNode 
match (locNode)<-[:HAS_BILLING_ADDRESS_AT]-(p:Provider)-[:HAS_SPECIALTY]->(t:TaxonomyCode) 
return distinct locNode.Address1 + ' ' + locNode.CityName +', ' + locNode.StateName as locationAddress, locNode.latitude as latitude, locNode.longitude as longitude, coalesce(p.BusinessName,p.FirstName + ' ' + p.LastName) as practiceName, p.NPI as NPI
order by locationAddress;

Pretty neat, right? As always, the source code as always is available on github.