<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Aglietti</title>
    <description>The latest articles on DEV Community by Michael Aglietti (@maglietti).</description>
    <link>https://dev.to/maglietti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F856691%2Fc59e93a7-01c8-466a-8b9e-3a5f73f9dcbe.jpeg</url>
      <title>DEV Community: Michael Aglietti</title>
      <link>https://dev.to/maglietti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maglietti"/>
    <language>en</language>
    <item>
      <title>Apache Ignite 3.1.0 is now available</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Mon, 03 Nov 2025 21:14:36 +0000</pubDate>
      <link>https://dev.to/maglietti/apache-ignite-310-is-now-available-36dn</link>
      <guid>https://dev.to/maglietti/apache-ignite-310-is-now-available-36dn</guid>
      <description>&lt;p&gt;Apache Ignite 3.1.0 is now available.&lt;/p&gt;

&lt;p&gt;This release includes 1,461 commits addressing 1,265 JIRA tickets with enhancements across SQL query performance, observability, client capabilities, and disaster recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Apache Ignite 3?
&lt;/h2&gt;

&lt;p&gt;Apache Ignite 3 is a memory-first distributed SQL database built for high-velocity data workloads where milliseconds matter and transaction windows keep shrinking.&lt;/p&gt;

&lt;p&gt;It handles transactions, queries, and processing in one place with strong consistency and flexible data models. Schema-driven colocation keeps related data together, achieving up to 40x performance improvement for relational queries. You get ultra-low-latency performance without managing multiple systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New in 3.1.0
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query performance improvements&lt;/strong&gt; reduce both network overhead and unnecessary data scanning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fine-grained data placement control&lt;/strong&gt; through distribution zones. Configure replica counts and data node filters per zone to optimize query performance based on your access patterns. This extends Ignite 3's schema-driven colocation with control over where replicas live and how many you need.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logical namespaces&lt;/strong&gt; through SQL schemas. Multiple tables with the same name can exist in different schemas, solving naming conflicts in shared environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Diagnose performance bottlenecks faster&lt;/strong&gt; with observability improvements. Monitor storage engine health, SQL query execution, replication lag, and clock drift across cluster nodes. Export metrics to logs with configurable filtering.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standard API support across all client platforms&lt;/strong&gt; makes Ignite 3 a drop-in replacement for traditional databases. .NET developers use ADO.NET provider. Python developers use DB API 2.0. Java developers use enhanced JDBC and Spring Data integration. All clients gain partition awareness and backward compatibility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reduced downtime during partition failures&lt;/strong&gt; with disaster recovery operations. Inspect partition states across the cluster, restart failed partitions, and trigger recovery through CLI or REST API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analytical SQL capabilities&lt;/strong&gt; through Calcite 1.40 upgrade. ROLLUP/CUBE hierarchical aggregations, NULLS FIRST/NULLS LAST ordering, and enhanced temporal data handling enable sophisticated reporting directly on operational data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Over 400 bug fixes&lt;/strong&gt; addressing data integrity, SQL query processing, and rebalancing operations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Download Ignite 3.1.0: &lt;a href="https://ignite.apache.org/download.cgi" rel="noopener noreferrer"&gt;https://ignite.apache.org/download.cgi&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Star Ignite 3 on GitHub: &lt;a href="https://github.com/apache/ignite-3" rel="noopener noreferrer"&gt;https://github.com/apache/ignite-3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get started with Ignite 3: &lt;a href="https://ignite.apache.org/docs/ignite3/latest/quick-start/getting-started-guide" rel="noopener noreferrer"&gt;https://ignite.apache.org/docs/ignite3/latest/quick-start/getting-started-guide&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Apache Ignite, Apache, and the flame logo are trademarks of The Apache Software Foundation.&lt;/p&gt;

</description>
      <category>database</category>
      <category>distributedsystems</category>
      <category>sql</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Monitoring Quine Streaming Graph using Grafana + InfluxDB</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Fri, 09 Jun 2023 15:12:16 +0000</pubDate>
      <link>https://dev.to/maglietti/monitoring-quine-streaming-graph-using-grafana-influxdb-4g8k</link>
      <guid>https://dev.to/maglietti/monitoring-quine-streaming-graph-using-grafana-influxdb-4g8k</guid>
      <description>&lt;h2&gt;
  
  
  Monitoring Data in Motion
&lt;/h2&gt;

&lt;p&gt;There has been a significant increase in the popularity of event streaming and stream processing applications/technologies within the data engineering community. With the accelerating growth of big data, IoT, and cloud computing, more organizations are facing the challenge of extracting actionable insights earlier in the event pipeline. For historical reasons, operational tools for monitoring, alerting, and diagnosing system issues are oriented toward data at rest. That doesn't mean they can't be just as useful for monitoring data in motion. It just means adjusting your monitoring regime to a streaming mindset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3ogjpqwcvuta4og9qc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3ogjpqwcvuta4og9qc2.png" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://a16z.com/2020/10/15/emerging-architectures-for-modern-data-infrastructure/#section--15" rel="noopener noreferrer"&gt;Emerging Architectures for Modern Data Infrastructure - Andreessen Horowitz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A good example of a next-gen streaming infrastructure element is Quine. Quine is an event streaming technology designed to process graph-shaped event streams and produce high-value events in real time.&lt;/p&gt;

&lt;p&gt;In this blog post, we'll guide you through setting up Grafana backed by InfluxDB to monitor a Quine instance. We'll show you how to configure Quine to send data to InfluxDB, create a dashboard in Grafana to visualize this data, and use Grafana's powerful features to detect issues and anomalies in real time. By the end of this post, you'll have a solid understanding of how to monitor event stream pipelines using Grafana and InfluxDB, and you'll be equipped with the tools and knowledge needed to keep Quine running smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Grafana and InfluxDB
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://grafana.com" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; is a tool that helps you visualize and understand operational metrics data. It lets you create visual dashboards to monitor and analyze data from sources across your data infrastructure. DevOps teams use Grafana metrics dashboards to make informed decisions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jdbxd0c0txujavnkd3k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jdbxd0c0txujavnkd3k.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The observability subsystem for Quine is build for Grafana integration.&lt;/p&gt;

&lt;p&gt;Above is an example of my typical development and testing environment when working on a &lt;a href="https://quine.io/recipes" rel="noopener noreferrer"&gt;recipe&lt;/a&gt;. The event sources and output sinks change depending on the scenario, but most of the time, I run Quine on my local host, configured to push metrics to InfluxDB and visualize the observations in Grafana. Using Docker containers makes it easy to configure and clean up my environment quickly.&lt;/p&gt;

&lt;p&gt;We need to do a little pre-work before launching the Docker containers. This is how I set up my environment using &lt;code&gt;docker-compose&lt;/code&gt;. You may do things differently based on how Docker is installed on your host.&lt;/p&gt;

&lt;p&gt;I like to keep &lt;code&gt;docker-compose.yaml&lt;/code&gt; files arranged inside their directories in a &lt;code&gt;docker&lt;/code&gt; directory that lives in &lt;code&gt;$HOME&lt;/code&gt;. This helps me keep things organized and makes sharing configs between my MacOS laptop and Ubuntu servers easy.&lt;/p&gt;

&lt;p&gt;I created a &lt;a href="https://quine-recipe-public.s3.us-west-2.amazonaws.com/quine-grafana-docker.zip" rel="noopener noreferrer"&gt;zip file&lt;/a&gt; of my config to download and use with the blog post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;
wget https://quine-recipe-public.s3.us-west-2.amazonaws.com/quine-grafana-docker.zip
unzip quine-grafana-docker.zip

Archive:  quine-docker.zip
  inflating: docker/cassandra/docker-compose.yaml
  inflating: docker/grafana/docker-compose.yaml
   creating: docker/grafana/grafana-provisioning/
   creating: docker/grafana/grafana-provisioning/datasources/
  inflating: docker/grafana/grafana-provisioning/datasources/datasource.yml
   creating: docker/grafana/grafana-provisioning/dashboards/
  inflating: docker/grafana/grafana-provisioning/dashboards/quine.json
  inflating: docker/grafana/grafana-provisioning/dashboards/dashboard.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: I included a &lt;code&gt;docker-compose&lt;/code&gt; file for Cassandra in the zip archive. I won't cover the Cassandra config in this article. The file is included as a reference if you choose to separate your persistent storage from the application to keep from competing for server resources. See the &lt;a href="https://quine.io/components/persistors/cassandra-setup/" rel="noopener noreferrer"&gt;Cassandra Persistor&lt;/a&gt; docs for a sample configuration file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You now have this directory structure in your &lt;code&gt;$HOME&lt;/code&gt; dir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker
├── cassandra
│   └── docker-compose.yaml
└── grafana
    ├── docker-compose.yaml
    └── grafana-provisioning
        ├── dashboards
        │   ├── dashboard.yaml
        │   └── quine.json
        └── datasources
            └── datasource.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Docker configured and the &lt;code&gt;quine-docker.zip&lt;/code&gt; files loaded on your virtualization host, it's time to start the containers so that they are ready to receive data from Quine.&lt;/p&gt;

&lt;p&gt;Change into the &lt;code&gt;grafana&lt;/code&gt; directory and start the InfluxDB/Grafana stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something similar to this appear in your terminal window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;+] Running 18/18
 ⠿ grafana Pulled                                            8.7s
   ⠿ f56be85fc22e Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;2.8s
   ⠿ 9efeca377709 Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;3.0s
   ⠿ b4608283f0dd Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;3.5s
   ⠿ 94ba646ecfcd Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;3.9s
   ⠿ 6730f2b3d4cf Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;4.1s
   ⠿ 871e090050be Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;4.4s
   ⠿ 03d60ad4c029 Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;5.7s
   ⠿ baaa3e79bf5c Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;7.6s
   ⠿ 01c0c058d3df Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;7.7s
 ⠿ influxdb Pulled                                           9.6s
   ⠿ 918547b94326 Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;7.4s
   ⠿ 5d79063a01c5 Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;7.7s
   ⠿ a8e9798c2a3f Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;7.8s
   ⠿ e8074b4fc936 Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;8.5s
   ⠿ a913b4722330 Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;8.5s
   ⠿ 9c8265b2cf7a Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;8.6s
   ⠿ 9037f1aeb9df Pull &lt;span class="nb"&gt;complete                              &lt;/span&gt;8.6s
&lt;span class="o"&gt;[&lt;/span&gt;+] Running 4/4
 ⠿ Volume &lt;span class="s2"&gt;"grafana_grafana-storage"&lt;/span&gt;   Created                0.0s
 ⠿ Volume &lt;span class="s2"&gt;"grafana_influxdb-storage"&lt;/span&gt;  Created                0.0s
 ⠿ Container grafana-influxdb-1       Started                0.5s
 ⠿ Container grafana-grafana-1        Started                0.7s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the containers are running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"table {{.Names}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.Status}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.Ports}}"&lt;/span&gt;
NAMES                 STATUS           PORTS
grafana-grafana-1     Up 4 seconds     0.0.0.0:3000-&amp;gt;3000/tcp
grafana-influxdb-1    Up 4 seconds     0.0.0.0:8086-&amp;gt;8086/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations! 🎉  InfluxDB and Grafana are running in separate containers and listening on their default ports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Quine to Send Metrics Data
&lt;/h2&gt;

&lt;p&gt;Enable metrics reporting in Quine via configuration parameters that can be passed as Java system properties with &lt;code&gt;-D&lt;/code&gt; or contained in a &lt;a href="https://quine.io/reference/config/configuration/" rel="noopener noreferrer"&gt;Quine configuration&lt;/a&gt; file. Quine can report metrics to &lt;code&gt;jmx&lt;/code&gt;, &lt;code&gt;csv&lt;/code&gt;, &lt;code&gt;influxdb&lt;/code&gt;, and &lt;code&gt;slf4j&lt;/code&gt;  for analysis. The &lt;code&gt;jmx&lt;/code&gt; metrics reporter is enabled by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Xmx12G&lt;/span&gt; &lt;span class="nt"&gt;-Xms12G&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Dquine&lt;/span&gt;.metrics-reporters.1.type&lt;span class="o"&gt;=&lt;/span&gt;influxdb &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Dquine&lt;/span&gt;.metrics-reporters.1.database&lt;span class="o"&gt;=&lt;/span&gt;db0 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Dquine&lt;/span&gt;.metrics-reporters.1.period&lt;span class="o"&gt;=&lt;/span&gt;30s &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-Dquine&lt;/span&gt;.metrics-reporters.1.host&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;container_host&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-jar&lt;/span&gt; quine-1.5.3.jar &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-r&lt;/span&gt; wikipedia &lt;span class="nt"&gt;--force-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of things to note when passing configuration as system properties.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-D&lt;/code&gt; parameters must come before &lt;code&gt;-jar&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;When launching Quine with a recipe (&lt;code&gt;-r&lt;/code&gt;) you also have to pass &lt;code&gt;--force-config&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alternatively, you can pass the following configuration stored in &lt;code&gt;quine-metrics.conf&lt;/code&gt; to Quine to accomplish the same thing.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;quine-metrics.conf&lt;/code&gt; file containing the HOCON configuration from the &lt;a href="https://quine.io/reference/config/configuration/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;quine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;# where metrics collected by the application should be reported&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;metrics-reporters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;# Report metrics to an influxdb (version 1) database&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;influxdb&lt;/span&gt;&lt;span class="w"&gt;

      &lt;/span&gt;&lt;span class="c1"&gt;# required by influxdb - the interval at which new records will&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;# be written to the database&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;period&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt;

      &lt;/span&gt;&lt;span class="c1"&gt;# Connection information for the influxdb database&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;database&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;db&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;scheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;container_host&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8086&lt;/span&gt;&lt;span class="w"&gt;

      &lt;/span&gt;&lt;span class="c1"&gt;# Authentication information for the influxdb database. Both&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;# fields may be omitted&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;# user = admin&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;# password = admin&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then launch Quine, passing the configuration file on the command line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-Dconfig&lt;/span&gt;.file&lt;span class="o"&gt;=&lt;/span&gt;metrics.conf &lt;span class="nt"&gt;-jar&lt;/span&gt; quine-1.5.4.jar &lt;span class="nt"&gt;-r&lt;/span&gt; wikipedia &lt;span class="nt"&gt;--force-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quine Metrics
&lt;/h2&gt;

&lt;p&gt;Quine reports three classes of metrics; counters, timers, and gauges.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When queried, the &lt;a href="https://quine.io/reference/rest-api/#/paths/api-v1-admin-metrics/get" rel="noopener noreferrer"&gt;metrics summary&lt;/a&gt; API endpoint reports the same metrics as a metrics reporter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Counters
&lt;/h3&gt;

&lt;p&gt;Quine uses counters to accumulate the number of times that events occur. Counters can return either a value or a histogram.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;node.edge-counts.*&lt;/code&gt;: Histogram-style summaries of edges per node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node.property-counts.*&lt;/code&gt;: Histogram-style summaries of properties per node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shard.*.sleep-counters&lt;/code&gt;: Count the lifecycle state of nodes managed by a shard&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Timers
&lt;/h3&gt;

&lt;p&gt;Quine reports the elapsed time in milliseconds it takes to perform persistor operations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;persistor.get-journal&lt;/code&gt;: Time taken to read and deserialize a single node's relevant journal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;persistor.persist-event&lt;/code&gt;: Time taken to serialize and persist one message's worth of on-node events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;persistor.get-latest-snapshot&lt;/code&gt;: Time taken to read (but not deserialize) a single node snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gauges
&lt;/h3&gt;

&lt;p&gt;Quine gauges report metrics as a value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memory.heap.*&lt;/code&gt;: JVM heap usage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.total&lt;/code&gt;: JVM combined memory usage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shared.valve.ingest&lt;/code&gt;: Number of current requests to slow ingest for another part of Quine to catch up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dgn-reg.count&lt;/code&gt;: Number of in-memory registered DomainGraphNodes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a Dashboard in Grafana
&lt;/h2&gt;

&lt;p&gt;A dashboard in Grafana contains a series of panels that provide an at-a-glance view of how Quine is performing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log into Grafana. The username and password for the container is admin:admin.&lt;/li&gt;
&lt;li&gt;Decide if you are going to keep the default password or skip changing it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you launched Grafana using the &lt;code&gt;docker-compose&lt;/code&gt; files from the &lt;code&gt;quine-docker.zip&lt;/code&gt; file that I provided, you will see a dashboard called  "Quine - Monitor a Recipe" in the lower left hand corner of the Dashboards card. Click on that dashboard to open it. Initially, the dashboard will be empty. It will fill in as you run a recipe.&lt;/p&gt;

&lt;p&gt;Let's start Quine with the Wikipedia recipe and the &lt;code&gt;metrics.conf&lt;/code&gt; file from above to get familiar with each visualization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-Dconfig&lt;/span&gt;.file&lt;span class="o"&gt;=&lt;/span&gt;metrics.conf &lt;span class="nt"&gt;-jar&lt;/span&gt; quine-1.5.3.jar &lt;span class="nt"&gt;-r&lt;/span&gt; wikipedia &lt;span class="nt"&gt;--force-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metrics will populate the dashboard after about 30 seconds once Quine is running. You may need to reload your browser to have Grafana pull all of the metrics from InfluxDB. Also, be sure to set the time range in the upper right corner of the dashboard to "Last 15 minutes" to ensure that you have a current time range selected to visualize.&lt;/p&gt;

&lt;p&gt;Your dashboard will begin to populate like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdfoz7159r1waj4zvt60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdfoz7159r1waj4zvt60.png" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Grafana dashboard view for Quine running the Wikipedia ingest recipe.&lt;/p&gt;

&lt;p&gt;Hover over each graph in the dashboard to expose a "three-dot" menu in the upper right hand corner of the panel. Click on the menu and select "edit" to review how each visualization is configured. Some visualizations use the query builder, and some are written directly as an InfluxDB query.&lt;/p&gt;

&lt;p&gt;Please modify the dashboard to match your environment and satisfy your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've Learned Monitoring Quine
&lt;/h2&gt;

&lt;p&gt;Monitoring a streaming graph is similar to any other database, with a few additional key metrics to watch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pay attention to supernodes! A super-node is a single node in the graph with a very large number of edge counts (tens of thousands and more). A single supernode or a moderate number of mid-sized supernodes will cause Quine to keep nodes awake continuously, which can lead to backpressure.&lt;/li&gt;
&lt;li&gt;Quine is backpressured, which means that the performance of the persistence subsystem affects the flow of events in the graph. Watch for when the shards fill with awake nodes and the associated persistor latency if you see a drop in the event ingest rate.&lt;/li&gt;
&lt;li&gt;Java garbage collection impacts backpressure. It is normal for Quine ingest rates to fluctuate as Java manages the heap. Keep an eye on when your heap consumption approaches the max memory configured for Java. I've found the best performance when launching Quine with a 12G (&lt;code&gt;-Xmx12G -Xms12G&lt;/code&gt;) memory allocation pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The metrics dashboard built into the Exploration UI is good for understanding how Quine is currently operating. However, monitoring the performance of a recipe or solution over time requires a DevOps tool like Grafana. This blog will get you up and running with a sample dashboard that replicates all of the gauges in the Exploration UI that you can modify to suit your needs.&lt;/p&gt;

&lt;p&gt;Did you run this dashboard in your environment? How did it perform? Start a conversation in &lt;a href="https://that.re/quine-slack" rel="noopener noreferrer"&gt;Slack&lt;/a&gt; with a screenshot of your dashboard or feedback to improve our base Grafana dashboard.&lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>streaminganalytics</category>
    </item>
    <item>
      <title>Calculate Risk and Optimize Asset Allocation in Real Time</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Fri, 09 Jun 2023 15:12:04 +0000</pubDate>
      <link>https://dev.to/maglietti/calculate-risk-and-optimize-asset-allocation-in-real-time-18he</link>
      <guid>https://dev.to/maglietti/calculate-risk-and-optimize-asset-allocation-in-real-time-18he</guid>
      <description>&lt;h2&gt;
  
  
  The Hidden Cost of Batch Processing for Financial Institutions
&lt;/h2&gt;

&lt;p&gt;The recent failures at financial institutions like First Republic Bank, Signature Bank, and even Silicon Valley Bank have brought issues of regulatory compliance and capital management to the forefront for both industry members and the wider public alike.&lt;/p&gt;

&lt;p&gt;One thing these events have exposed is that the financial industry largely relies on an approach to managing mandated operational risk capital requirements, batch processing, that is ill-suited to the direction both the market and compliance are heading. Operationally, batch processing is time-consuming, costly, and often must take place in constrained time windows between market close and open.&lt;/p&gt;

&lt;p&gt;The knock on financial effect of the operation limitations of batch processing are more impactful: institutions are slow to react to changing market conditions, which can lead to over- or under-allocation of certain classes of funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time Risk Calculation and Asset Allocation
&lt;/h2&gt;

&lt;p&gt;Using Quine streaming graph, financial institutions can respond to market changes in real time, providing adequate coverage for risk exposure while ensuring compliance minimally affects asset allocation.&lt;/p&gt;

&lt;p&gt;At a high level, Quine accomplishes this by doing what it does best: combining multiple feeds in real-time to build hierarchical models of elements like markets, trading entities, risk classes, and asset values, that adjust in real time to changing market conditions.&lt;/p&gt;

&lt;p&gt;At a specific level, we have created a Quine recipe that demonstrates, in the context of regulatory monitoring requirements like the &lt;a href="https://www.bis.org/basel_framework/standard/LCR.htm" rel="noopener noreferrer"&gt;Basel III Liquidity Coverage Ratio (LCR)&lt;/a&gt;, &lt;a href="https://www.bis.org/bcbs/publ/d295.htm" rel="noopener noreferrer"&gt;Net Stable Funding Ratio (NSFR)&lt;/a&gt; and liquidity risk monitoring tools as described in &lt;a href="https://www.bis.org/bcbs/basel3.htm" rel="noopener noreferrer"&gt;https://www.bis.org/bcbs/basel3.htm&lt;/a&gt;, the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calculating risk while taking into account complex interdependencies and rules.&lt;/li&gt;
&lt;li&gt;Constantly recomputing liquidity-indexed risk to determine capital requirements relative to market conditions.&lt;/li&gt;
&lt;li&gt;Normalizing multiple sources to calculate relative value of assets and roll up the results to determine near-real time liquidity in event liquidation is necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfespf7szwylkjxndn7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfespf7szwylkjxndn7r.png" alt="A graph that shows the hierarchical nature of the graph for this particular problem domain." width="800" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A sample view from the graph this recipe generates.&lt;/p&gt;

&lt;p&gt;The recipe can be found &lt;a href="https://quine.io/recipes/finance/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quine Developer Site 2.0
&lt;/h2&gt;

&lt;p&gt;As part of our continued focus on improving the Quine developer experience, we’ve made significant changes to the Quine.io site.&lt;/p&gt;

&lt;p&gt;The most notable change is a total restructuring of the recipe pages to interleave code and contextual or documentary information. Recipe documentation now includes a full walkthrough of a recipe and an explanation of how the recipe works so that recipes can also act as training material.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z29r19trj4k1e3aqg29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z29r19trj4k1e3aqg29.png" alt="Recipe page example showing sidebar navigation, sections that include Scenario, How it Works, and a breakdown of standing and ingest queries, and a link to the full recipe." width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new, more structured recipe page.&lt;/p&gt;

&lt;p&gt;Other changes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improved developer journey by separating tutorials (&lt;a href="https://quine.io/getting-started/" rel="noopener noreferrer"&gt;getting started&lt;/a&gt;), &lt;a href="https://quine.io/docs/" rel="noopener noreferrer"&gt;technical docs&lt;/a&gt;, and &lt;a href="https://quine.io/recipes/" rel="noopener noreferrer"&gt;recipe docs&lt;/a&gt; into their own sections of the site&lt;/li&gt;
&lt;li&gt;Full release notes and release history included in the &lt;a href="https://quine.io/download/" rel="noopener noreferrer"&gt;downloads&lt;/a&gt; page&lt;/li&gt;
&lt;li&gt;Direct links to Quine blog posts, events, and self-service demos are now on the &lt;a href="https://quine.io/info/" rel="noopener noreferrer"&gt;info&lt;/a&gt; page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can still download and easily get started with Quine (hint, hint) and we’d love to &lt;a href="https://that.re/quine-slack" rel="noopener noreferrer"&gt;hear your feedback&lt;/a&gt; and add features you think might help you build great things with Quine.&lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>streaminganalytics</category>
    </item>
    <item>
      <title>Create a Quine Icon Library with Python</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Tue, 02 May 2023 20:46:16 +0000</pubDate>
      <link>https://dev.to/maglietti/create-a-quine-icon-library-with-python-11md</link>
      <guid>https://dev.to/maglietti/create-a-quine-icon-library-with-python-11md</guid>
      <description>&lt;p&gt;Have you ever wanted to add flair to a graph visualization but are unsure which icons Quine supports? In this blog, we explore a Python script that fetches valid icon names from the web, configures the Exploration UI, then creates a graph of icon nodes for reference. The script uses several popular Python libraries, including Requests, BeautifulSoup, and Halo, along with the &lt;code&gt;/query-ui&lt;/code&gt; and &lt;code&gt;/query/cypher&lt;/code&gt; API endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Environment
&lt;/h2&gt;

&lt;p&gt;Before we start, we need to ensure that we have the necessary libraries installed. We will be using &lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;beautifulsoup4&lt;/code&gt;, &lt;code&gt;log_symbols&lt;/code&gt;, and &lt;code&gt;halo&lt;/code&gt;. You can install them using &lt;code&gt;pip&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/thatdot/quine/releases/latest" rel="noopener noreferrer"&gt;Quine&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Python 3&lt;/li&gt;
&lt;li&gt;Requests library (&lt;code&gt;pip install requests&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;BeautifulSoup library (&lt;code&gt;pip install beautifulsoup4&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Optional Halo library for operation visuals  (&lt;code&gt;pip install log-symbols halo&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start Quine so that it is ready to run the script.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;java -jar quine-1.5.3.jar&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;The script begins by importing the required libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;halo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Halo&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;log_symbols&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LogSymbols&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build a list of icon names
&lt;/h2&gt;

&lt;p&gt;We use the &lt;code&gt;requests&lt;/code&gt; library to GET the webpage referenced in the &lt;a href="https://docs.quine.io/reference/rest-api.html#/paths/api-v1-query-ui-node-appearances/put" rel="noopener noreferrer"&gt;Replace Node Appearances&lt;/a&gt; API documentation. Quine supports version 2.0.0 of the &lt;a href="https://ionic.io/ionicons/v2/cheatsheet.html" rel="noopener noreferrer"&gt;Ionicons&lt;/a&gt; icon set from the Ionic Framework. The link contains a list of 733 icons supported by Quine. A &lt;code&gt;try...except&lt;/code&gt; block handles any errors that might occur during the request. If the request is successful, the script saves the HTML content of the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://ionic.io/ionicons/v2/cheatsheet.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogSymbols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET Icon Cheatsheet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we use BeautifulSoup to parse the HTML content of the page to extract all of the icon names. The &lt;code&gt;soup.select&lt;/code&gt; method finds all &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; elements with a &lt;code&gt;name&lt;/code&gt; attribute and returns a list, which are then looped over to extract the &lt;code&gt;value&lt;/code&gt; attribute of each tag later. We output &lt;code&gt;len(all_icons)&lt;/code&gt; to verify that we identified all of the icons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;all_icons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogSymbols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract Icon Names:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_icons&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create Node Appearances
&lt;/h2&gt;

&lt;p&gt;Now that we have the icon names, we can use them to create node appearances for the Quine Exploration UI. We'll use the &lt;code&gt;json&lt;/code&gt; package to format the &lt;code&gt;nodeAppearances&lt;/code&gt; data as JSON, and &lt;code&gt;requests&lt;/code&gt; to replace the current &lt;code&gt;nodeAppearances&lt;/code&gt; with a PUT to the &lt;code&gt;/query-ui/node-appearances&lt;/code&gt; endpoint. We wrap the API call in &lt;code&gt;try...expect&lt;/code&gt; as before to handle any errors. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;predicate&lt;/code&gt;: filter which nodes to apply this style&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;size&lt;/code&gt;: the size of the icon in pixels&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;icon&lt;/code&gt;: the name of the icon&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;label&lt;/code&gt;: the label of the node&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Cypher does not allow dash (&lt;code&gt;-&lt;/code&gt;) characters in node labels. We get around this by replacing all of the dashes with underscores in the node labels.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;nodeAppearances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;predicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;propertyKeys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;knownValues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbLabel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;40.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;icon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;icon_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_icons&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;json_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodeAppearances&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/api/v1/query-ui/node-appearances&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogSymbols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT Node Appearances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create Icon Nodes
&lt;/h2&gt;

&lt;p&gt;Finally, our script creates icon nodes by sending a series of POST requests to the Quine &lt;code&gt;/query/cypher&lt;/code&gt; endpoint. For each icon name, a Cypher query creates the corresponding icon node and connects it to the appropriate group node. We use &lt;code&gt;Halo&lt;/code&gt; to create a spinner while we POST the icon data to Quine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;quineSpinner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Halo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Creating Icon Nodes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spinner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bouncingBar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;quineSpinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;icon_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_icons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;query_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MATCH (a), (b), (c) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WHERE id(a) = idFrom(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;  AND id(b) = idFrom(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;  AND id(c) = idFrom(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET a:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, a.name = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET b:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, b.name = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET c:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, c.name = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CREATE (a)&amp;lt;-[:` `]-(b)&amp;lt;-[:` `]-(c)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MATCH (a), (c) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WHERE id(a) = idFrom(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND id(c) = idFrom(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET a:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, a.name = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SET c:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, c.name = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;icon_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CREATE (a)&amp;lt;-[:` `]-(c)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;quineSpinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_text&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;# print(query_text)
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/api/v1/query/cypher&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;quineSpinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;succeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST Icon Nodes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;quineSpinner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Request Timeout: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running the script
&lt;/h2&gt;

&lt;p&gt;At this point, we are ready to run the script and visualize the icons supported in Quine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python3 iconLibrary.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The script updates the console as it moves through the blocks of code that we described above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;✔ GET Icon Cheatsheet
✔ Extract Icon Names: 733
✔ PUT Node Appearances
✔ POST Icon Nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to Quine in your browser and load all of the nodes that we just created into the Exploration UI. There are multiple ways to load all of the nodes in the UI, for this example, we use &lt;code&gt;MATCH (n) RETURN n&lt;/code&gt;. The Exploration UI will warn that you are about to render 787 nodes which is correct for all of the icons and grouping nodes generated by the script. Hit the OK button to view the graph. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: If you already had Quine open in a browser before running the script, you will need to refresh your browser window to load the new &lt;code&gt;nodeAppearances&lt;/code&gt; submitted by the query in order for the nodes to render correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our case, the nodes are jumbled when they are first rendered. Click the play button in the top nav to have Quine organize the graph. Our result produced the graph visualization of all supported icons below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgraarq318mu9i14e1jf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgraarq318mu9i14e1jf4.png" alt="Quine Icons" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There you have it, a graph visualization using all of the icons Quine supports!&lt;/p&gt;

&lt;p&gt;This script can generate the &lt;code&gt;nodeAppearances&lt;/code&gt; graph and serve as a starting point if you are looking to automate fetching non-streaming data from websites to enrich streaming data stored in Quine. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9rs1f01jkpdn0tdh6gf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9rs1f01jkpdn0tdh6gf.png" alt="Quine Interactive API Docs" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to learn more about Quine or explore using other API libraries with Quine, check out the interactive &lt;a href="https://docs.quine.io/reference/rest-api.html#/" rel="noopener noreferrer"&gt;REST API documentation&lt;/a&gt; available via the document icon in the left nav bar. The interactive documentation is a great place to submit API requests. Code samples in popular languages are quickly mocked up in the docs for use when experimenting with small projects like this yourself.&lt;/p&gt;

&lt;p&gt;You can download this script and try it for yourself in this &lt;a href="https://github.com/maglietti/quine-iconLibrary" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>python</category>
    </item>
    <item>
      <title>Dynamic Duo: Quine &amp; Novelty Detector for Insider Threats</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Thu, 20 Apr 2023 22:03:48 +0000</pubDate>
      <link>https://dev.to/maglietti/dynamic-duo-quine-novelty-detector-for-insider-threats-2fod</link>
      <guid>https://dev.to/maglietti/dynamic-duo-quine-novelty-detector-for-insider-threats-2fod</guid>
      <description>&lt;h2&gt;
  
  
  Adding Quine to the Insider Threat Detection Proof of Concept
&lt;/h2&gt;

&lt;p&gt;A lot has changed since we first posted the &lt;a href="https://www.thatdot.com/blog/stop-insider-threats-with-automated-behavioral-anomaly-detection" rel="noopener noreferrer"&gt;Stop Insider Threats With Automated Behavioral Anomaly Detection&lt;/a&gt; blog post. Most significantly, thatDot released Quine, our streaming graph, as an &lt;a href="https://www.thatdot.com/blog/announcing-open-source-release-of-quine-streaming-graph" rel="noopener noreferrer"&gt;open source project&lt;/a&gt; just as the industry is recognizing the value of real-time ETL and complex event processing in service of business requirements. This is especially true in finance and cybersecurity, where minutes (seconds or even milliseconds) can mean the difference between disaster, survival or success.&lt;/p&gt;

&lt;p&gt;Our goal, at the time, was to show how anomaly detection on &lt;a href="https://www.thatdot.com/blog/whats-the-difference-between-categorical-and-numerical-data" rel="noopener noreferrer"&gt;categorical data&lt;/a&gt; could be used to resolve complex challenges utilizing an industry recognized standard benchmark dataset, which happened to be static. The approach we used then was to pre-process (batch) the &lt;a href="https://www.osti.gov/biblio/1001546" rel="noopener noreferrer"&gt;VAST Insider Threat challenge dataset &lt;/a&gt;with Python then ingest that processed stream of data with thatDot's Novelty Detector to identity the bad actor.&lt;/p&gt;

&lt;p&gt;But with a new tool in our kit we decided to see what would be involved in updating the workflow by replacing the Python pre-processing, instead using Quine in front of Novelty Detector in our pipeline.  &lt;/p&gt;

&lt;p&gt;This involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Defining the &lt;a href="https://docs.quine.io/getting-started/ingest-streams-tutorial.html" rel="noopener noreferrer"&gt;ingest queries&lt;/a&gt; required to consume and shape the VAST datasets; and&lt;/li&gt;
&lt;li&gt; Developing a&lt;a href="https://docs.quine.io/getting-started/standing-queries-tutorial.html" rel="noopener noreferrer"&gt; standing query&lt;/a&gt; to output the data to Novelty Detector for anomaly detection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Data from the &lt;a href="https://that.re/insider-threat" rel="noopener noreferrer"&gt;dataset&lt;/a&gt; is broken into three files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Employee to office and source IP address mapping in employeeData.csv
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="na"&gt;ingestStreams&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;employeeData.csv&lt;/span&gt;
    &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;61&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherCsv&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (employee), (ipAddress), (office)&lt;/span&gt;
        &lt;span class="s"&gt;WHERE id(employee) = idFrom('employee', $that.EmployeeID)&lt;/span&gt;
          &lt;span class="s"&gt;AND id(ipAddress) = idFrom('ipAddress',$that.IP)&lt;/span&gt;
          &lt;span class="s"&gt;AND id(office) = idFrom('office',$that.Office)&lt;/span&gt;

        &lt;span class="s"&gt;SET employee.id = $that.EmployeeID,&lt;/span&gt;
            &lt;span class="s"&gt;employee:employee&lt;/span&gt;

        &lt;span class="s"&gt;SET ipAddress.ip = $that.IP,&lt;/span&gt;
            &lt;span class="s"&gt;ipAddress:ipAddress&lt;/span&gt;

        &lt;span class="s"&gt;SET office.office = $that.Office,&lt;/span&gt;
            &lt;span class="s"&gt;office:office&lt;/span&gt;

        &lt;span class="s"&gt;CREATE (ipAddress)&amp;lt;-[:USES_IP]-(employee)-[:SHARES_OFFICE]-&amp;gt;(office)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  Proximity reader data from door badge scanners in proxLog.csv
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxLog.csv&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherCsv&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (employee), (badgeStatus)&lt;/span&gt;
        &lt;span class="s"&gt;WHERE id(employee) = idFrom('employee', $that.ID)&lt;/span&gt;
          &lt;span class="s"&gt;AND id(badgeStatus) = idFrom('badgeStatus',$that.ID,$that.Datetime,$that.Type,$that.ID)&lt;/span&gt;

        &lt;span class="s"&gt;SET employee.id = $that.ID,&lt;/span&gt;
            &lt;span class="s"&gt;employee:employee&lt;/span&gt;

        &lt;span class="s"&gt;SET badgeStatus.type = $that.Type,&lt;/span&gt;
            &lt;span class="s"&gt;badgeStatus.employee = $that.ID,&lt;/span&gt;
            &lt;span class="s"&gt;badgeStatus.datetime = $that.Datetime,&lt;/span&gt;
            &lt;span class="s"&gt;badgeStatus:badgeStatus&lt;/span&gt;

        &lt;span class="s"&gt;CREATE (employee)-[:BADGED]-&amp;gt;(badgeStatus)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  Network traffic in IPLog3.5.csv
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt; &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
    &lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IPLog3.5.csv&lt;/span&gt;
    &lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherCsv&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (ipAddress), (request)&lt;/span&gt;
        &lt;span class="s"&gt;WHERE id(ipAddress) = idFrom('ipAddress',$that.SourceIP)&lt;/span&gt;
          &lt;span class="s"&gt;AND id (request) = idFrom('request', $that.SourceIP,$that.AccessTime, $that.DestIP, $that.Socket)&lt;/span&gt;

        &lt;span class="s"&gt;SET request.reqSize = $that.ReqSize,&lt;/span&gt;
            &lt;span class="s"&gt;request.respSize = $that.RespSize,&lt;/span&gt;
            &lt;span class="s"&gt;request.datetime = $that.AccessTime,&lt;/span&gt;
            &lt;span class="s"&gt;request.dst = $that.DestIP,&lt;/span&gt;
            &lt;span class="s"&gt;request.dstport = $that.Socket,&lt;/span&gt;
            &lt;span class="s"&gt;request:request&lt;/span&gt;

        &lt;span class="s"&gt;SET ipAddress.ip = $that.SourceIP,&lt;/span&gt;
            &lt;span class="s"&gt;ipAddress:ipAddress&lt;/span&gt;

        &lt;span class="s"&gt;CREATE (ipAddress)-[:MADE_REQUEST]-&amp;gt;(request)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These ingests form a basic structure that looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwzmqwo2p5t0ibp7mq2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwzmqwo2p5t0ibp7mq2y.png" alt="A snapshot of the graph created by ingest streams showing Employee 51 connected by the Badged edge to a door reader event node and node IP address by USES_IP edge, which is connected to a Request node by a Made_request edge." width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ingest streams combine to create the essential graph structure.&lt;/p&gt;

&lt;p&gt;Because we have created an intuitive schema for identifying nodes by way of feeding &lt;code&gt;idFrom()&lt;/code&gt; deterministic &lt;em&gt;and&lt;/em&gt; descriptive data that can be used to query for them very efficiently (and do so with sub-millisecond latency).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F301lw5klmw9d8cqspd70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F301lw5klmw9d8cqspd70.png" alt="The same basic graph as above but this time showing a very efficient query for node properties. " width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick query efficiently displays relevant properties from connected nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving from Batch to Real Time Monitoring
&lt;/h2&gt;

&lt;p&gt;While this is certainly an improvement from our previous workflow, it is still highly manual (i.e., having to explicitly query for the data we're looking for). The promise of a Quine to Novelty Detector workflow is automation with real-time results.&lt;/p&gt;

&lt;p&gt;By ingesting the data in chronological order (as presented in the source files), we are able to easily match proximity network events to the last associated proximity badge event &lt;em&gt;in real-time&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is accomplished via standing query matches like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;standingQueries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
         &lt;span class="s"&gt;MATCH (request)&amp;lt;-[:MADE_REQUEST]-(ipAddress)&amp;lt;-[:USES_IP]-(employee)-[:BADGED]-&amp;gt;(badgeStatus)&lt;/span&gt;
         &lt;span class="s"&gt;RETURN DISTINCT id(request) AS requestid&lt;/span&gt;
       &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cypher&lt;/span&gt;
     &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;print-output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherQuery&lt;/span&gt;
         &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;MATCH (request)&amp;lt;-[:MADE_REQUEST]-(ipAddress)&amp;lt;-[:USES_IP]-(employee)-[:BADGED]-&amp;gt;(badgeStatus)&lt;/span&gt;
          &lt;span class="s"&gt;WHERE id(request) = $that.data.requestid&lt;/span&gt;
            &lt;span class="s"&gt;AND badgeStatus.datetime&amp;lt;=request.datetime&lt;/span&gt;
          &lt;span class="s"&gt;WITH max(badgeStatus.datetime) AS date, request, ipAddress&lt;/span&gt;
          &lt;span class="s"&gt;MATCH (request)&amp;lt;-[:MADE_REQUEST]-(ipAddress)&amp;lt;-[:USES_IP]-(employee)-[:BADGED]-&amp;gt;(badgeStatus)&lt;/span&gt;
          &lt;span class="s"&gt;WHERE badgeStatus.datetime=date&lt;/span&gt;

          &lt;span class="s"&gt;RETURN badgeStatus.type AS status,ipAddress.ip AS src,request.dstport AS port,request.dst AS dst&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question remains, "How do we share the standing query matches from Quine to Novelty Detector?" This can be done in a number of ways (all via &lt;a href="https://docs.quine.io/getting-started/standing-queries-tutorial.html" rel="noopener noreferrer"&gt;standing query outputs)&lt;/a&gt; including, but not limited to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Writing results to a file that Novelty Detector ingests;&lt;/li&gt;
&lt;li&gt; Emitting webhooks from Quine to Novelty Detector; or&lt;/li&gt;
&lt;li&gt; Publishing results to a Kafka topic to be ingested by Novelty Detector.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Although the first two choices will work, they are severely suboptimal. Consider a simple example of a single employee's data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdiiv8r5ch2qr8kdb00y8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdiiv8r5ch2qr8kdb00y8.png" alt="A graph showing employee's data that renders as thousands of nodes connected to four main clusters. " width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Visualizing data from a single employee.&lt;/p&gt;

&lt;p&gt;Writing the aggregate 115,434 matches would be done one record at a time (on each standing query match) to the filesystem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;andThen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
           &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WriteToFile&lt;/span&gt;
           &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;behaviors.jsonl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using webhooks suffer the same issue as writing to file, and introduces induced latency from the HTTP transactions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;andThen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostToEndpoint&lt;/span&gt;
            &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080/api/v1/novelty/behaviors/observe?transformation=behaviors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ultimately, we settled on the third option as it most closely resembles production environments, and is the most performant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;andThen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
           &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WriteToKafka&lt;/span&gt;
           &lt;span class="na"&gt;bootstrapServers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:9092&lt;/span&gt;
           &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vast&lt;/span&gt;
           &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;JSON&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The big question - did it work?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1r6p778ze8cn80iw2pf7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1r6p778ze8cn80iw2pf7.png" alt="A scatter graph of Novelty Detector results showing the anomalous behavior connected to a compromised faciltiy." width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Results from the Novelty Detector UI.&lt;/p&gt;

&lt;p&gt;Absolutely.&lt;/p&gt;

&lt;p&gt;The anomalous activity has been identified.&lt;/p&gt;

&lt;p&gt;Was it worthwhile?&lt;/p&gt;

&lt;p&gt;Sure, but...&lt;/p&gt;

&lt;h2&gt;
  
  
  It Don't Mean a Thing If It Ain't Got That Real-Time Swing
&lt;/h2&gt;

&lt;p&gt;Although we were able to accomplish the same results with Quine in a single step this was still a batch processing-based exercise. The true value of a Quine to Novelty Detector pipeline is in the melding of complex event stream processing in Quine with shallow learning (no training data) techniques in Novelty Detector, providing an efficient solution for detecting persistent threats and unwanted behaviors in your network. This pattern, moving from batch processing, requiring heavy lifting and grooming of datasets, to real-time stream processing is one where Quine and Novelty Detector thrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it Yourself
&lt;/h2&gt;

&lt;p&gt;If you'd like to try the VAST test case yourself, you can run &lt;a href="https://aws.amazon.com/marketplace/pp/prodview-jo6mbktt5ptzm" rel="noopener noreferrer"&gt;Novelty Detector on AWS&lt;/a&gt; with a generous free usage tier. Instructions for &lt;a href="https://www.thatdot.com/product/novelty-detector-docs/novelty-configuration-guide" rel="noopener noreferrer"&gt;configuring Novelty Detector&lt;/a&gt; are available here.\&lt;br&gt;
And the open source version of Quine is available for &lt;a href="https://quine.io/download" rel="noopener noreferrer"&gt;download here&lt;/a&gt;. If you are interested there is also an &lt;a href="https://www.thatdot.com/product/quine-enterprise" rel="noopener noreferrer"&gt;enterprise version &lt;/a&gt;that offers clustering for horizontal scaling and resilience.&lt;/p&gt;

&lt;p&gt;And if you'd prefer a demo or have additional questions, check out &lt;a href="https://that.re/quine-slack" rel="noopener noreferrer"&gt;Quine community slack &lt;/a&gt;or &lt;a href="mailto:info@thatdot.com"&gt;send us an email&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>idFrom(): the simple function that’s key to Quine streaming graph</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Thu, 20 Apr 2023 22:03:28 +0000</pubDate>
      <link>https://dev.to/maglietti/idfrom-the-simple-function-thats-key-to-quine-streaming-graph-p2g</link>
      <guid>https://dev.to/maglietti/idfrom-the-simple-function-thats-key-to-quine-streaming-graph-p2g</guid>
      <description>&lt;h2&gt;
  
  
  A simple concept at the core of a new way of processing data
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;What's a streaming graph?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When we first released Quine streaming graph last year, we had to answer this question a lot. After all, a "streaming graph" had never existed before.&lt;/p&gt;

&lt;p&gt;As interest grew, we got pretty good at answering, usually something like this: Quine is a real-time event processor like Flink or ksqlDB. It consumes data from sources like Kafka and Kinesis, queries for complex patterns in event streams, and pushes results to the next hop in the streaming architecture the instant a match is made. However, unlike those venerable systems, Quine uses graph data structure.&lt;/p&gt;

&lt;p&gt;Hence, streaming graph.&lt;/p&gt;

&lt;p&gt;That seemed to work and, engineers being a curious lot, led inevitably to a second question: "How's it different from a graph database?"&lt;/p&gt;

&lt;p&gt;That's a fun question to answer, because it means we get to talk about&lt;code&gt;idFrom()&lt;/code&gt;. And explaining &lt;code&gt;idFrom()&lt;/code&gt; allows us to begin to unpack all the interesting architectural properties that make Quine uniquely well-suited for real-time complex event processing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8b9bj85nqpmhmkpqs59u.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8b9bj85nqpmhmkpqs59u.jpeg" alt="A photo of the character of David from the film Prometheus contemplating a scientific discovery. " width="640" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Big things have small beginnings." -- David from the film &lt;em&gt;Prometheus (2012)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-driven: what if we stopped querying databases?
&lt;/h2&gt;

&lt;p&gt;Unlike a graph database, which relies on an index to query for the existence of data in the graph, Quine uses &lt;code&gt;idFrom()&lt;/code&gt;, a custom &lt;a href="https://docs.quine.io/reference/cypher/cypher-language.html" rel="noopener noreferrer"&gt;Cypher function&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;idFrom()&lt;/code&gt; generates a unique node ID from a set of user-provided arguments -- most commonly taken from the data in the event stream itself -- which is then used in lieu of an index to locate and operate on a node and its properties. (We will get to the why in a bit but it will help first to look at how you use &lt;code&gt;idFrom()&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Say you want to analyze an event stream of edits from wikipedia to keep an eye out for edits made by specific authors to specific articles in specific databases.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;json&lt;/em&gt; record (a pared back version of the actual Wikipedia event feed used in the Wikipedia API recipe featured in our docs &lt;a href="https://docs.quine.io/getting-started/ingest-streams-tutorial.html" rel="noopener noreferrer"&gt;example here&lt;/a&gt;) might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/mediawiki/revision/create/1.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wikidatawiki"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"page_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;83996749&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"rev_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1869025669&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"rev_timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-04-05T18:18:23Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"performer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"user_is_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6135162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"rev_parent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1869025663&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create the nodes in a continuous stream of records, you would use &lt;code&gt;MATCH&lt;/code&gt; to declare the node names then call the &lt;code&gt;idFrom()&lt;/code&gt; function to generate unique node IDs based on the values in the json itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;pageNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;dbNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parentNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'revision'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.rev_id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.page_id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.database&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.performer.user_id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parentNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'revision'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.rev_parent_id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For now, we can skip adding properties to nodes but it helps our discussion to complete this simple graph by adding relationships between the nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:IN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbNode&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
       &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageNode&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
       &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:MADE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
       &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parentNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:NEXT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, as each event streams in, Quine will create and connect nodes, forming the desired subgraph that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5u9jjndsdwx6plm0vi9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5u9jjndsdwx6plm0vi9.png" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the same subgraph with node ID no longer concealed by the node labels:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31z8htcpglo6jotbnddb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31z8htcpglo6jotbnddb.png" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the things you didn't have to do to create this graph:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Query to find out if the node exists already before  &lt;/li&gt;
&lt;li&gt; Consult a schema&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Quine eliminates the need to check to see if the node exists before completing an operation.&lt;/p&gt;

&lt;p&gt;The deterministic nature of node IDs created using &lt;code&gt;idFrom()&lt;/code&gt; means a value or combination of values passed to the function will always result in the same ID.&lt;/p&gt;

&lt;p&gt;It will either create a new node based on the value or, if that node already exists, update it.&lt;/p&gt;

&lt;p&gt;In the latter case, because Quine is an event-sourced system, when Quine updates a node, it doesn't need to look up if the node already exists. Quine appends the update to the existing node, preserving historical versions that can be retrieved using &lt;code&gt;idFrom()&lt;/code&gt; with the at.time&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;idFrom()&lt;/code&gt; and CRUD operations: why Quine is so dang fast
&lt;/h2&gt;

&lt;p&gt;Inasmuch as Quine uses a hash of a value to generate a node ID that is then used for CRUD operations, it bears a superficial similarity between NoSQL key-value stores As long as you know either the ID or the value, it is dead simple to retrieve data from the graph.&lt;/p&gt;

&lt;p&gt;However, because of Quine's in-memory graph structure, it is far more efficient and performant operating on patterns, ranges (e.g. time-ordered), or otherwise related data than key-value databases.&lt;/p&gt;

&lt;p&gt;Using the node ID to anchor the query, you specify the edges to traverse to find connected data.&lt;/p&gt;

&lt;p&gt;This might be a query to retrieve a node's properties using node ID (in this case, for &lt;code&gt;revNode&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MATCH (n) WHERE strId(n) = "8b290926-271c-3497-b5d6-e30fcf934a73" RETURN id(n), properties(n)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which delivers these results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnl7skrkq10xknb8howr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgnl7skrkq10xknb8howr.png" alt="Screen shot of a node ID and associated properties in json format." width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don't know a node's ID, you can query for it using the node's properties and the &lt;code&gt;strid()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;userNode:&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;user_is_bot:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;strid&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftytz402kyjf80v7asre2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftytz402kyjf80v7asre2.png" alt="Screen shot of a node strID() results -- the node ID as a string." width="800" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But what about more complex queries -- for example, a query that must retrieve multiple related objects. Key-value stores are famously inefficient in this scenario. But this is precisely where Quine's architectural choices come in. Using an in-memory graph structure means you can query for any node in a subgraph, follow it's edges, and produce one or more values.&lt;/p&gt;

&lt;p&gt;For example, say you want to find all revisions where a bot made an update to the 'wikidatawiki' database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;userNode:&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;user_is_bot:&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:MADE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;revNode:&lt;/span&gt;&lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;pageNode:&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:IN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;dbNode:&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"wikidatawiki"&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotd305boxaq0o3x8vd8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotd305boxaq0o3x8vd8c.png" alt="The results of the query -- two nod IDs returned." width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Either way, it starts with setting the node ID with &lt;code&gt;idFrom()&lt;/code&gt; . And &lt;code&gt;idFrom()&lt;/code&gt; makes Quine &lt;a href="https://www.thatdot.com/blog/scaling-quine-streaming-graph-to-process-1-million-events-sec" rel="noopener noreferrer"&gt;very, very fast.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing queries and querying data from the future with &lt;code&gt;idFrom()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Standing queries persist inside the graph, monitoring the stream for specific patterns. Propagate them throughout the graph without you ever having to issue a query again. Standing queries persist, monitoring for matches.&lt;/p&gt;

&lt;p&gt;Once matches are found, standing queries trigger actions using those results (e.g. report results, execute code, transform other data in the graph, publish data to another source).&lt;/p&gt;

&lt;p&gt;To do this, every standing query must have two parts, the &lt;code&gt;pattern&lt;/code&gt; portion (what sub-graph you are matching for in the event stream) and the &lt;code&gt;outputs&lt;/code&gt; portion (the action you wish to take).&lt;/p&gt;

&lt;p&gt;Adapted from the recipe used in &lt;a href="https://docs.quine.io/getting-started/standing-queries-tutorial.html" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt;, here's a standing query that monitors for non-bot revisions to the 'enwiki' database and outputs these events to the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;standingQueries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (userNode:user {user_is_bot: false})-[:MADE]-&amp;gt;(revNode:revision {database: 'enwiki'})&lt;/span&gt;
        &lt;span class="s"&gt;RETURN DISTINCT id(revNode) as id&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cypher&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;print-output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherQuery&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;MATCH (n)&lt;/span&gt;
          &lt;span class="s"&gt;WHERE id(n) = $that.data.id&lt;/span&gt;
          &lt;span class="s"&gt;RETURN properties(n)&lt;/span&gt;
        &lt;span class="na"&gt;andThen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PrintToStandardOut&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6d7cgpcudxfkztxgqcp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6d7cgpcudxfkztxgqcp.png" alt="results (json) prining to the console." width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Standing query matches printing to console.&lt;/p&gt;

&lt;p&gt;Because standing queries persist in the graph, incrementally updating partial results as new data arrives, you are not just querying the past and present state, you are setting up queries  for data yet to arrive.&lt;/p&gt;

&lt;p&gt;And while &lt;code&gt;idFrom()&lt;/code&gt; is a key part of what makes standing queries possible, to really understand what makes Quine function so efficiently as a stream processor, we'll need to dive into the actor-based, graph-shaped compute model. But that's for a different post.&lt;/p&gt;

&lt;p&gt;Instead, I'll leave you with a clever use of &lt;code&gt;idFrom()&lt;/code&gt; employed by developers at a SaaS company that uses Quine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5xfrk5fxeam4236w305.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5xfrk5fxeam4236w305.png" alt="Color wheel, Color, Pie chart" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Partitioning Key Spaces for a SaaS application using &lt;code&gt;idFrom()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Since you can generate a node ID by passing an arbitrary combination of values to &lt;code&gt;idFrom()&lt;/code&gt;, some Quine users with SaaS or internal multi-tenant applications have employed it to partition graphs by customer namespace or similar property.&lt;/p&gt;

&lt;p&gt;Sticking with the Wikipedia example, you could create distinct sub-graphs corresponding to each of the database types by adding &lt;code&gt;$that.database&lt;/code&gt; as an additional value determining each node ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;  &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;pageNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;dbNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;),(&lt;/span&gt;&lt;span class="n"&gt;parentNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'revision'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.rev_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.database&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
         &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.page_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.database&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
         &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'db'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.database&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
         &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.performer.user_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.database&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
         &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parentNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'revision'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.rev_parent_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;$that.database&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a series of subgraphs partitioned by database and would allow you to be certain that if you query for data related to a specific database, you won't inadvertently return data from others.&lt;/p&gt;

&lt;p&gt;And while the chance of key collision exists, it is &lt;a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions" rel="noopener noreferrer"&gt;vanishingly small&lt;/a&gt;, making this approach suitable for use in multi-tenant SaaS applications.&lt;/p&gt;

&lt;p&gt;At any rate, this accomplished what the company wanted: a partitioned graph for data separation, all standing and ad hoc queries work the same across the entire graph, and the only real cost is the discipline of always using the compound key.&lt;/p&gt;

&lt;p&gt;Pretty clever.&lt;/p&gt;

&lt;p&gt;If any of this inspires you or piques your interest and you want to try Quine yourself, check out &lt;a href="https://docs.quine.io/getting-started/" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; docs.&lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>streaminganalytics</category>
    </item>
    <item>
      <title>Quine, streaming graph data that scales past 1 million events/second</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Fri, 07 Oct 2022 20:11:20 +0000</pubDate>
      <link>https://dev.to/maglietti/quine-streaming-graph-data-that-scales-past-1-million-eventssecond-534j</link>
      <guid>https://dev.to/maglietti/quine-streaming-graph-data-that-scales-past-1-million-eventssecond-534j</guid>
      <description>&lt;p&gt;Finding relationships within categorical data is graph's strong point. Doing so at scale, as Quine now makes possible, has significant implications for cyber security, fraud detection, observability, logistics, e-commerce, and any use case that graph is both well-suited for and which must process high velocity data in real time.&lt;/p&gt;

&lt;p&gt;The goal of this test is to demonstrate a high-volume of sustained event ingest that is resilient to cluster node failure in both Quine and the persister using commodity infrastructure, and to share performance and cost results along with details of the test for those interested in either reproducing results or running Quine in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5lecd3trvn64wh2qv0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5lecd3trvn64wh2qv0l.png" alt="Scaling to 1M+ events/second and demonstrating recovery from various failure scenarios" width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our tests delivered the following results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1M events/second processed for a 2 hour period&lt;/li&gt;
&lt;li&gt;1M+ writes per second&lt;/li&gt;
&lt;li&gt;1M 4-node graph traversals (reads) per second&lt;/li&gt;
&lt;li&gt;21K results (4-node pattern matches) emitted per second&lt;/li&gt;
&lt;li&gt;140 commodity hosts plus 1 hot spare running Quine Enterprise&lt;/li&gt;
&lt;li&gt;66 storage hosts using Apache Cassandra persistor&lt;/li&gt;
&lt;li&gt;3 hosts for Apache Kafka&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Infrastructure
&lt;/h2&gt;

&lt;p&gt;The Cassandra persistor layer’s settings are set at a TTL of 15 minutes and a replication factor of 1 to manage quota limits and spending on cloud infrastructure. This does not fit every possible use case, but it is fairly common. Other scenarios which are more data-storage oriented will often increase the replication factor and/or TTL. In those variations, maintaining the 1 million events/sec processing rate would require increasing the number of Cassandra hosts or disk storage, both of which are budgetary concerns more than technical concerns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt; Component &lt;/th&gt;
            &lt;th&gt;# of Hosts&lt;/th&gt;
            &lt;th&gt;Host Types&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td&gt;Quine Cluster&lt;/td&gt;
            &lt;td&gt;141&lt;/td&gt;
            &lt;td&gt;
                &lt;ul&gt;
                    &lt;li&gt;c2-standard-30 (30 vCPUs, 120GB RAM)&lt;/li&gt;
                    &lt;li&gt;Max heap for JVM set to 12GB&lt;/li&gt;
                    &lt;li&gt;140 cluster size w/ 1 hot spare&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;Cassandra Persistor Cluster&lt;/td&gt;
            &lt;td&gt;66&lt;/td&gt;
            &lt;td&gt;
                &lt;ul&gt;
                    &lt;li&gt;n1-highmem-32 (32 vCPU, 208GB RAM)&lt;/li&gt;
                    &lt;li&gt;x 375 GB local SSD each&lt;/li&gt;
                    &lt;li&gt;r1 x 375 GB local SSD each&lt;/li&gt;
                    &lt;li&gt;durable_writes=false&lt;/li&gt;
                    &lt;li&gt;TTL=15 minutes on snapshots (to control disk costs in testing) and journals tables&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;Kafka&lt;/td&gt;
            &lt;td&gt;3&lt;/td&gt;
            &lt;td&gt;
                &lt;ul&gt;
                    &lt;li&gt;n2-standard-4 (4 vCPU, 16 GB RAM)&lt;/li&gt;
                    &lt;li&gt;Preloaded with 8 billion events (sufficient for a sustained 2 hour ingest at 1 million events
                        per second)&lt;/li&gt;
                    &lt;li&gt;420 partitions&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Test
&lt;/h2&gt;

&lt;p&gt;The plan is set out below, with each action labeled and the results explained. Events are clearly marked by sequence # on the Grafana screen grabs below the table.&lt;/p&gt;

&lt;p&gt;A few notes on the test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://github.com/thatdot/1m-scripts/blob/improve-dx/quine/ingest.py" rel="noopener noreferrer"&gt;script&lt;/a&gt; is used to generate events&lt;/li&gt;
&lt;li&gt;Host failures are manually triggered.&lt;/li&gt;
&lt;li&gt;We used Grafana for the results (and screenshots).&lt;/li&gt;
&lt;li&gt;We pre-loaded Kafka with enough events to sustain &lt;strong&gt;one million events/second&lt;/strong&gt; for two hours.&lt;/li&gt;
&lt;li&gt;A Cassandra cluster is used for persistent data storage. The Cassandra cluster is not over-provisioned to accommodate compaction intentionally (a common strategy) so that the effects of database maintenance on the ingest rate can be demonstrated.&lt;/li&gt;
&lt;li&gt;The cluster is run in a Kubernetes environment&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Seq #&lt;/th&gt;
      &lt;th&gt;Actions&lt;/th&gt;
      &lt;th&gt;Expected Results&lt;/th&gt;
      &lt;th&gt;Actual Results&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;
        1
      &lt;/td&gt;
      &lt;td&gt;
        Start the Quine cluster and begin ingest from Kafka
      &lt;/td&gt;
      &lt;td&gt;
        The ingest rate increase and settle at or above 1 million events per second
      &lt;/td&gt;
      &lt;td&gt;
        Observed
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        2
      &lt;/td&gt;
      &lt;td&gt;
        Let Quine run for 40 minutes to establish a stable baseline
      &lt;/td&gt;
      &lt;td&gt;
        Quine does not fail and maintains a baseline ingest rate at or above 1 million events per second.
      &lt;/td&gt;
      &lt;td&gt;
        Observed
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        3
      &lt;/td&gt;
      &lt;td&gt;
        Kill a Quine host
      &lt;/td&gt;
      &lt;td&gt;
        Quine ingest is not significantly impacted. The hot spare steps in to recover quickly, and Kubernetes replaces the killed host which becomes a new hot spare.
      &lt;/td&gt;
      &lt;td&gt;
        Observed at 17:47. No impact to ingest rate. The hot spare recovered quickly and ingest was not impacted.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        4
      &lt;/td&gt;
      &lt;td&gt;
        Persistor Maintenance
      &lt;/td&gt;
      &lt;td&gt;
        Cassandra regularly performs maintenance, Quine experiences this as increased latency and should backpressure the ingest to maintain stability during database maintenance.
      &lt;/td&gt;
      &lt;td&gt;
        From 17:55 - 18:15 the ingest rate is reduced as a corresponding increase in latency is measured above 1ms across all nodes from the Cassandra persistor.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        5
      &lt;/td&gt;
      &lt;td&gt;
        Kill two Quine hosts
      &lt;/td&gt;
      &lt;td&gt;
        Observe the following sequence: hot spare recovers one host, whole cluster suspends ingest due to being degraded, Kubernetes replaces killed hosts, first replaced host recovers the cluster, and the second replaced host becomes the new hot spare.
      &lt;/td&gt;
      &lt;td&gt;
        Observed from 18:18 - 18:25. Due to Kubernetes the impact was not visible. However, the expected sequence was confirmed in the logs.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        6
      &lt;/td&gt;
      &lt;td&gt;
        Stop and resume a Quine host for about 1 minute to inject high latency
      &lt;/td&gt;
      &lt;td&gt;
        Quine detects the host is no longer available, boots it from the cluster, and hot spare steps in to recover. When the rejected host resumes, it learns it was removed from the cluster, so it shuts down, is restarted by Kubernetes, and to become the new hot spare
      &lt;/td&gt;
      &lt;td&gt;
        Observed from 18:41 - 18:46. No impact to ingest rate as the back pressured ingest was for a single host in the cluster, and the recovery happened quickly.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        7
      &lt;/td&gt;
      &lt;td&gt;
        Stop and resume a Cassandra persistor host for about 1 minute to inject high latency
      &lt;/td&gt;
      &lt;td&gt;
        Quine back pressures ingest until Cassandra persistor has recovered
      &lt;/td&gt;
      &lt;td&gt;
        Observed from 18:47 - 18:54. Due to replication factor = 1, ingest was impacted until Cassandra persistor recovered. Then ingest resumed to &amp;gt; 1M e/s.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        8
      &lt;/td&gt;
      &lt;td&gt;
        Kill a Cassandra persistor host
      &lt;/td&gt;
      &lt;td&gt;
        Quine suspends ingest until Cassandra persistor recovers with a new host
      &lt;/td&gt;
      &lt;td&gt;
        Observed from 18:54 - 19:10. The host was recovered quickly due to kubernetes, and ingest briefly recovered to 1M e/s by 18:58 (only a few minutes).
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        9
      &lt;/td&gt;
      &lt;td&gt;
        Persistor Maintenance
      &lt;/td&gt;
      &lt;td&gt;
        Cassandra regularly performs maintenance, Quine experiences this as increased latency and should backpressure the ingest to maintain stability during database maintenance.
      &lt;/td&gt;
      &lt;td&gt;
        From 17:55 - 18:15 the ingest rate is reduced as a corresponding increase in latency is measured above 1ms across all nodes from the Cassandra persistor.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;
        10
      &lt;/td&gt;
      &lt;td&gt;
        Let Quine consume the remaining Kafka stream
      &lt;/td&gt;
      &lt;td&gt;
        Observe the Quine hosts drop to zero events per second (not all at once)
      &lt;/td&gt;
      &lt;td&gt;
        Observed from 19:10 - 19:35. Around the time Cassandra persistor latency was returning to 1ms, and ingest returned to 1M e/s, the pre-loaded ingest stream began to become exhausted on some hosts. For the following 20 minutes hosts exhausted their partitions in the stream.
      &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr441cxrybeoqjkddqada.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr441cxrybeoqjkddqada.png" alt="A diagram showing sustained event processing of 1 million events per second." width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see from the overall ingest rate results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;#1 shows an initial peak of 1.25M events/sec&lt;/li&gt;
&lt;li&gt;#2 Quine settles into a steady ingest rate &amp;gt; 1 million events/sec&lt;/li&gt;
&lt;li&gt;#3 Quine recovers nicely after single node shutdown&lt;/li&gt;
&lt;li&gt;Quine settles into a steady ingest rate &amp;gt; 1 million events/sec&lt;/li&gt;
&lt;li&gt;#s 4 and 9 show Cassandra maintenance event (see Cassandra Latency - Figure 3)&lt;/li&gt;
&lt;li&gt;#5  Quine has no problem with two-node  failure events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We observed that a persistor node high-latency event (7) has a more marked impact on performance than either a Quine node failure (5) or an outright failure of a persistor node (8). In the case of a clear failure,  Kubernetes is quick to replace the node, allowing ingest to resume. In cases when a persistence node state is non-responsive but not clearly down, Quine’s response is to back pressure ingest until the node is recovered.&lt;/p&gt;

&lt;p&gt;An alternate variation on this test could use more persistor machines to stabilize ingest rates during maintenance events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbisjxm8apzm5mqiug3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbisjxm8apzm5mqiug3y.png" alt="A per-host diagram demonstrates when different operational events impacted performance of both individual hosts and the overall cluster." width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The individual Quine node ingest graphs indicate when individual nodes are offline and reinforces the observation that Quine Enterprise’s cluster resilience allows for smooth operation during high-volume ingest, even in the face of a Quine node shut down or failure. Quine’s overall performance, and hence an area of operational focus for anyone planning a production deployment, more closely conforms with persistor performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8e86ayvk8e8yp5wantzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8e86ayvk8e8yp5wantzl.png" alt="Cassandra Latency events line up with overall decreases in cluster performance." width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The median query latency for the Cassandra cluster during this test was &amp;lt;1 ms. Even during/following persistor shutdown (8) or node failure (7), cluster latency stayed &amp;lt; 1.5 ms. Events at (1), (5), and (8), all reflect increased latency for single nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing Queries and 1 Million 4-node traversals per second
&lt;/h2&gt;

&lt;p&gt;The purpose of running any complex event processor, Quine included, is in detecting and acting on high-value events in real time. This could mean detecting indications of a cyber attack, or video stream buffering, or identifying e-commerce upsell opportunities at check out. This is where Quine really excels.&lt;/p&gt;

&lt;p&gt;Standing queries are a unique feature of Quine. They monitor streams for specified patterns, maintaining partial matches, and executing user-specified actions the instant a full match is made. Actions can include anything from updating the graph itself by creating new nodes or edges, writing results out to Kafka (or Kinesis, or posting results to a webhook).&lt;/p&gt;

&lt;p&gt;In this test, Quine standing queries monitored for specific 4-node patterns requiring a 4-node traversal every time an event was ingested. Traditional graph databases slow down ingest when performing multi-node traversal. Not Quine. Quine’s ability to sustain high-speed data ingest together with simultaneous graph analysis is a revolutionary new capability. Not only did Quine ingest more than 1,000,000 events per second, it analyzed all that data in real-time to find more than 20,000 matches per second for complex graph patterns. This is a whole new world!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Quine Hitting 1 Million Events/Sec Matters
&lt;/h2&gt;

&lt;p&gt;Since its release in 2007 at the start of the NoSQL revolution, Neo4J have proven conclusively the value of graph to connect and find complex patterns in categorical data.&lt;/p&gt;

&lt;p&gt;The graph data model is indispensable to everything from fraud detection to network observability to cybersecurity. It is used for recommendation engines, logistics, and XDR/EDR.&lt;/p&gt;

&lt;p&gt;But not long after NoSQL hit the scene, Kafka kicked off the movement toward real-time event processing. Soon, event processors like Flink, Spark Streaming and ksqlDB brought the ability to process live streams. These systems relied on less-expressive key-value stores or slower document and relational databases to save intermediate data.&lt;/p&gt;

&lt;p&gt;Quine is the graph analog and is important because now you can do what graph is really good at -- finding complex patterns across multiple streams of data using not just numerical but categorical data.&lt;/p&gt;

&lt;p&gt;Quine makes all the great graph use cases viable at high volumes and in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;If you want to reproduce this test, we have published the &lt;a href="https://github.com/thatdot/1m-scripts" rel="noopener noreferrer"&gt;test details on Github&lt;/a&gt; so that you can understand and run it yourself..&lt;/p&gt;

&lt;p&gt;If you want help planning your own test, or you would like to try the Quine Enterprise, please contact us. You can also read more about &lt;a href="https://www.thatdot.com/product/quine-enterprise" rel="noopener noreferrer"&gt;Quine Enterprise here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Or you can start learning about Quine now by visiting the &lt;a href="https://quine.io/" rel="noopener noreferrer"&gt;Quine open source project&lt;/a&gt;. We have a Slack channel where folks can ask questions and we are always up for a call.&lt;/p&gt;

</description>
      <category>quine</category>
      <category>streamingdata</category>
      <category>graph</category>
    </item>
    <item>
      <title>Use Quine Graph ETL to reduce SIEM storage costs.</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Mon, 25 Jul 2022 19:57:01 +0000</pubDate>
      <link>https://dev.to/maglietti/use-quine-graph-etl-to-reduce-siem-storage-costs-4g0e</link>
      <guid>https://dev.to/maglietti/use-quine-graph-etl-to-reduce-siem-storage-costs-4g0e</guid>
      <description>&lt;h2&gt;
  
  
  The High Cost of Storing Low Value Data
&lt;/h2&gt;

&lt;p&gt;The high cost of SIEM has given rise to countless &lt;a href="https://www.google.com/search?q=%22SIEM%22+reduce+cost" rel="noopener noreferrer"&gt;articles and dozens of companies&lt;/a&gt; promoting strategies or products to reduce monthly bills, with some claiming 50-90% reductions.While the 50-90% number seems a little overblown and sure to be met with skepticism — enterprises tend to take a “better to store it and pay the price than regret we didn’t later” approach, especially when the data may have compliance implications — the appeal is easy to understand.&lt;/p&gt;

&lt;p&gt;I took a look at the current methods for reducing SIEM costs and compared them to what graph ETL using Quine can accomplish all while considering impact on data fidelity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State of Stream Pre-Processing: Random, Destructive, and Only Somewhat Effective
&lt;/h2&gt;

&lt;p&gt;Legacy event log pre-processing offerings typically employ one or more of six basic strategies to reduce the amount of data stored in the SIEM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Sample data&lt;/li&gt;
&lt;li&gt; Filter out fields&lt;/li&gt;
&lt;li&gt; Filter out events&lt;/li&gt;
&lt;li&gt; De-duplicate&lt;/li&gt;
&lt;li&gt; Aggregate/roll-up&lt;/li&gt;
&lt;li&gt; Re-route some data to cheaper alternatives for cold storage (e.g,. Logstash or Amazon S3)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These solutions also usually include the ability to set rules that refine system behavior by data source or event type – for instance, sampling one in five events from a log of failed authentication attempts but one in twenty events from an Apache access log.&lt;/p&gt;

&lt;p&gt;It is important to note that stream pre-processing can only be applied to each stream and each record individually. Since many modern event processing use cases — not just SIEM but those for machine learning and e-commerce — depend on combining multiple data sources to model complex events, the single-stream approach means storing duplicate data from each stream required to connect them later (in SQL terms, these data are the keys used to join the various data sets once they are stored).  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We were paying for 600 GB to 700 GB per day with Splunk, which meant we were lousy co-workers to our IT group, because we had to tell them, 'Send us this field, not that field,' and limit the data ingestion severely," said John Gerber, principal cybersecurity analyst at Reston, Va., systems integrator SAIC. -- from &lt;a href="http://elastic%20siem%20woos%20enterprises%20with%20cost%20savings/" rel="noopener noreferrer"&gt;Elastic SIEM woos enterprises with cost savings&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As the quote above makes clear, some approaches also require lots of operational intervention, meaning delays for analysts and data scientists and an overall increase in cost of ownership.&lt;/p&gt;

&lt;p&gt;The more important limitation is that these approaches &lt;strong&gt;&lt;em&gt;cannot determine the value of the data they discard&lt;/em&gt;&lt;/strong&gt;. They either throw data away or, in the case of aggregation, &lt;a href="https://pubmed.ncbi.nlm.nih.gov/25810242/" rel="noopener noreferrer"&gt;reduce fidelity&lt;/a&gt;. All data is considered to have the same value.&lt;/p&gt;

&lt;p&gt;Quine’s approach is different: it turns high volumes of low-value data into low volumes of high-value data.&lt;/p&gt;

&lt;p&gt;Instead of storing data in Splunk or a similar system and then determining value, Quine can evaluate data as it arrives and make choices to store or discard based on the problem you are trying to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quine Ingest Queries: Semantic ETL for High Value Data
&lt;/h2&gt;

&lt;p&gt;At the heart of how Quine processes data are two query types: ingest and standing queries (more on the latter below).&lt;/p&gt;

&lt;p&gt;Quine uses ingest queries to consume event data and construct your streaming graph database. Ingest queries perform real-time ETL on incoming streams, combining multiple data sources (for example from multiple Kafka topics, Kinesis streams, data from databases, &lt;a href="https://www.thatdot.com/blog/ingesting-data-from-the-internet" rel="noopener noreferrer"&gt;live feeds via APIs&lt;/a&gt;) into a single streaming graph, eliminating the need to keep duplicate data around for joins.&lt;/p&gt;

&lt;p&gt;Using Quine’s ingest ETL, you can join all the data, eliminating cross-data stream duplicates. That accounts for some incremental data reduction over existing methods, which along with the other five strategies (all of which Quine supports) means Quine offers superior savings on your SIEM costs. But more than just deduplicating data, joining streams lets you draw conclusions early about what makes some data more valuable than other data.&lt;/p&gt;

&lt;p&gt;Quine’s real power, however, is its ability to apply a semantic filter to your data to find patterns made up of multiple events. And it does so as data streams in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save Only the Patterns That Matter
&lt;/h2&gt;

&lt;p&gt;Ingest queries make it easy to organize the high value, often complex, patterns in data into graph structures. These patterns are characterized by the relationships between multiple events. In a practical sense, you are shaping the data into a form that anticipates the analysis you will perform downstream in your SIEM. Quine can join, interpret, and trim away any data not relevant to the answers.&lt;/p&gt;

&lt;p&gt;What you end up creating in your graph-ETL are subgraphs, or patterns of two or more nodes and connecting edges.&lt;/p&gt;

&lt;p&gt;Here are a few real world examples from the Quine community:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find and store all instances where there have been attempts (both successful and failed) to log into the accounts of members of the executive team from multiple IP addresses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30yxpsx5p7el0zsse46q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30yxpsx5p7el0zsse46q.png" alt="A graph with three node types -- IP address, Account, and Executive Team." width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A subgraph for monitoring authentication fraud attempts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find and store all instances where multiple processes in different office locations are sending message to the same IP address&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m0zj0o3bxdtiwomsloa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3m0zj0o3bxdtiwomsloa.png" alt="A security-focused subgraph." width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A subgraph for monitoring processes and the IP addresses to which they write.&lt;/p&gt;

&lt;p&gt;In both of these examples, the test for what you keep and what you discard is based on what might possibly be important, on what matters to your business.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if data takes time to become interesting?
&lt;/h2&gt;

&lt;p&gt;One challenge processing streaming data – especially when event data arrives from many networked sources – is that it can arrive late or out of order, obscuring what would otherwise be an interesting pattern. Consider the examples above.&lt;/p&gt;

&lt;p&gt;What if the login attempts in example one were spread out over days or even weeks?&lt;/p&gt;

&lt;p&gt;What if log events from several locations in example two (above) were delayed for several hours or started at different times? Quine handles this late arriving data (as well as out of order data) using standing queries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgte3dws319whflfbvf7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgte3dws319whflfbvf7.png" alt="A five step diagram of a standing query in a graph." width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Standing queries persist in the graph, storing partial matches and triggering actions when a full match occurs.&lt;/p&gt;

&lt;p&gt;Standing queries live inside the graph and automatically propagate the incremental results computed from both historical data and incoming streaming data. Once matches are found, standing queries trigger actions using those results (e.g., execute code, transform other data in the graph, publish data to another system like Apache Kafka or Kinesis).&lt;/p&gt;

&lt;p&gt;The implication for SIEM storage reduction is that Quine can temporarily retain &lt;em&gt;possibly interesting&lt;/em&gt; incomplete patterns until a match occurs. It is neither discarded nor taking up costly space in your SIEM. Then, at the instant the match occurs, it is sent along to the SIEM system for regular processing. If a match doesn’t occur within a useful period, the data can be discarded automatically.&lt;/p&gt;

&lt;p&gt;Want to go further? Consider bypassing your SIEM altogether and sending alerts and data directly to your SOC or NOC’s dashboards, analysts, or data science team as it arrives and matches occur. But that’s for the next blog post. Until then, try out Quine’s graph ETL on your own log data. It is open source and easy to get started with.&lt;/p&gt;

&lt;p&gt;Who knows, it might just save you a few million dollars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to try it on your own logs, here are some resources to help:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download Quine - &lt;a href="https://quine.io/download" rel="noopener noreferrer"&gt;JAR file&lt;/a&gt; | &lt;a href="https://hub.docker.com/r/thatdot/quine" rel="noopener noreferrer"&gt;Docker Image&lt;/a&gt; | &lt;a href="https://github.com/thatdot/quine" rel="noopener noreferrer"&gt;Github&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check out the &lt;a href="https://www.thatdot.com/blog?*=Ingest" rel="noopener noreferrer"&gt;Ingest Data into Quine&lt;/a&gt; blog series covering everything from &lt;a href="https://docs.quine.io/components/ingest-sources/kafka.html" rel="noopener noreferrer"&gt;ingest from Kafka&lt;/a&gt; to ingesting .CSV data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://quine.io/recipes/apache-log-analytics" rel="noopener noreferrer"&gt;Apache Log Recipe&lt;/a&gt; - this recipe provides more ingest pattern examples&lt;/li&gt;
&lt;li&gt;Join &lt;a href="https://that.re/quine-slack" rel="noopener noreferrer"&gt;Quine Community Slack&lt;/a&gt; and get help from thatDot engineers and community members.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>streaminganalytics</category>
      <category>siem</category>
    </item>
    <item>
      <title>Standing Queries: Turning Event-Driven Data Into Data-Driven Events</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Wed, 06 Jul 2022 21:55:13 +0000</pubDate>
      <link>https://dev.to/maglietti/standing-queries-turning-data-driven-events-into-event-driven-data-29en</link>
      <guid>https://dev.to/maglietti/standing-queries-turning-data-driven-events-into-event-driven-data-29en</guid>
      <description>&lt;p&gt;Quine's super power is the ability to store and execute business logic within the graph. That query can then operate directly on data as it streams in. We call this type of query a &lt;em&gt;standing query&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A standing query incrementally matches some graph structure while new data is ingested into the graph. Quine’s special design makes this process extremely fast and efficient. When a full pattern match is found, a standing query takes action.&lt;/p&gt;

&lt;p&gt;A standing query is defined in two parts: a &lt;strong&gt;pattern&lt;/strong&gt; and an &lt;strong&gt;output&lt;/strong&gt;. The &lt;strong&gt;pattern&lt;/strong&gt; defines what we want to match, expressed in Cypher using the form &lt;code&gt;MATCH … WHERE … RETURN …&lt;/code&gt;. The &lt;strong&gt;output&lt;/strong&gt; defines the action(s) to take for each result produced by the &lt;code&gt;RETURN&lt;/code&gt;  in the pattern query.&lt;/p&gt;

&lt;p&gt;The result of a standing query output is passed to a series of actions which process the &lt;strong&gt;output&lt;/strong&gt;. This output can be logged, passed to other systems (via Kafka, Kinesis, HTTP POST, and more), or can even be used to perform additional actions like running new queries or even rewriting parts of the graph. Whatever logic your application needs. &lt;/p&gt;

&lt;h2&gt;
  
  
  How nodes match patterns
&lt;/h2&gt;

&lt;p&gt;Each node in Quine is backed by an actor, which makes each graph node act like its own little CPU. Actors function as lightweight, single-threaded logical computation units that maintain state and communicate with each other by passing messages.&lt;/p&gt;

&lt;p&gt;The actor model enables you to execute a standing query that is stored in the graph and remembered automatically. When you issue a &lt;code&gt;DistinctId&lt;/code&gt; standing query, the query is broken into individual steps that can be tested one at a time on individual nodes. Quine stores the result of each successive decomposition of a query (smaller and smaller queries) internally on the node issuing that portion of the query. The previous node's query is essentially a subscription to the next nodes status as either matching the query or not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq9ysm1w1dwpaq8rztze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq9ysm1w1dwpaq8rztze.png" alt="Quine Streaming Graph Actor Model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any changes in the next node’s pattern match state result in a notification to the querying node. In this way, a complex query is relayed through the graph, where each node subscribes to whether or not the next node fulfills its part of the query. When a complete match is made, or unmade, the chain is notified with results and an output action is triggered. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are two pattern match modes &lt;code&gt;DistinctId&lt;/code&gt; and &lt;code&gt;MultipleValues&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This must take the form of &lt;code&gt;MATCH WHERE RETURN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;When the &lt;code&gt;mode&lt;/code&gt; is &lt;code&gt;DistinctId&lt;/code&gt;, the pattern query &lt;code&gt;RETURN&lt;/code&gt; must also be &lt;code&gt;DISTINCT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.quine.io/components/multiple-values.html#match-query" rel="noopener noreferrer"&gt;https://docs.quine.io/components/multiple-values.html#match-query&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Creating a standing query
&lt;/h2&gt;

&lt;p&gt;The first step to making a Standing Query is determining the graph pattern you want to watch for. You may have deployed Quine in your data pipeline to perform a series of tasks to isolate data, implement a specific feature, or monitor the stream to find a specific pattern in real time. In any case, Quine will implement your logic using Cypher. The &lt;a href="https://github.com/thatdot/quine/blob/main/quine/recipes/sq-test.yaml" rel="noopener noreferrer"&gt;recipe&lt;/a&gt; for this example is included in the Quine repo if you'd like to follow along.  &lt;/p&gt;

&lt;p&gt;Let's demonstrate this concept using Quine's built in synthetic data generator that was introduced in v1.3.0. Say that you have a need to establish the relationships between all numbers in a number line and any number that is divisible by 10 using &lt;a href="https://mathworld.wolfram.com/IntegerDivision.html" rel="noopener noreferrer"&gt;integer division&lt;/a&gt; (where dividing always returns a whole number; the remainder is discarded).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="py"&gt;ingestStreams:&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="py"&gt;format:&lt;/span&gt;
      &lt;span class="py"&gt;query:&lt;/span&gt; &lt;span class="o"&gt;|-&lt;/span&gt;
        &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;gen.node.from&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$that&lt;/span&gt;&lt;span class="ss"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
             &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$that&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
        &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thisNode&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextNode&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thisNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
          &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
          &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;this.i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;this.prop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gen.string.from&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thisNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:next&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextNode&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; 
               &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thisNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:div_by_ten&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;divNode&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
      &lt;span class="py"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;CypherLine&lt;/span&gt;
    &lt;span class="py"&gt;type:&lt;/span&gt; &lt;span class="n"&gt;NumberIteratorIngest&lt;/span&gt;
    &lt;span class="nl"&gt;ingestLimit&lt;/span&gt;&lt;span class="dl"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creates a graph with 100000 nodes and a shape that we can use for our example. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fij0bbdtdn8luzwrf455c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fij0bbdtdn8luzwrf455c.png" alt="Graph Shape" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example above, I want to count the unique times that a pattern like the one visualized above occurs in a sample of 100000 numbers. A key to our pattern is the existence of the "data" parameter in a node that is generated by the &lt;code&gt;gen.string.from()&lt;/code&gt; function. &lt;/p&gt;

&lt;p&gt;To detect a pattern in our data, we can write a Cypher query in the &lt;code&gt;pattern&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="py"&gt;standingQueries:&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="py"&gt;pattern:&lt;/span&gt;
      &lt;span class="py"&gt;query:&lt;/span&gt; &lt;span class="o"&gt;|-&lt;/span&gt;
        &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:div_by_ten&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:div_by_ten&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c.prop&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
      &lt;span class="py"&gt;type:&lt;/span&gt; &lt;span class="k"&gt;Cypher&lt;/span&gt;
    &lt;span class="py"&gt;outputs:&lt;/span&gt;
      &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="py"&gt;results:&lt;/span&gt;
        &lt;span class="py"&gt;type:&lt;/span&gt; &lt;span class="k"&gt;Drop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is looking for a number which is the ten-divisor of another number which is also the ten-divisor of a number in the graph. That basically means it's looking for one of the first 1000 nodes created by our "number iterator" ingest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ java &lt;span class="nt"&gt;-jar&lt;/span&gt; quine &lt;span class="nt"&gt;-r&lt;/span&gt; sq-test.yaml
Graph is ready
Running Recipe Standing Query Test Recipe
Using 1 node appearances
Using 11 quick queries 
Running Standing Query STANDING-1
Running Ingest Stream INGEST-1
Quine web server available at http://0.0.0.0:8080
INGEST-1 status is completed and ingested 100000

 | &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; STANDING-1 count 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example simply counts how many are detected, using the standing query &lt;code&gt;output&lt;/code&gt; variant:  &lt;code&gt;type: Drop&lt;/code&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Standing query result output
&lt;/h2&gt;

&lt;p&gt;Say that instead of just counting the number of times that the pattern matches, we need to output the match for debugging or inspection. We can replace the &lt;code&gt;Drop&lt;/code&gt; output with a &lt;code&gt;CypherQuery&lt;/code&gt; that uses the matched result and then prints information to the console. When issuing a &lt;code&gt;DistinctId&lt;/code&gt; standing query, the result of a match is a payload that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isPositiveMatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resultId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2a757517-1225-7fe2-0d0e-22625ad3be37"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"a.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"a.prop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YH32SISr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"b.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4511&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"b.prop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fqx8aVAU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"c.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;451&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"c.prop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"61mTZqH8"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This payload includes the ID of the node that initially matched in the &lt;code&gt;data&lt;/code&gt; field. So We can write a new Cypher query to go fetch additional information triggered by this match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:div_by_ten&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:div_by_ten&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;$that.data.id&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;a.i&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a.prop&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.i&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.prop&lt;/span&gt; &lt;span class="n"&gt;c.i&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c.prop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;MATCH&lt;/code&gt; portion looks similar to our standing query, but this time we're not monitoring the graph, we're fetching data from the three-node pattern rooted at &lt;code&gt;(c)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Replacing the &lt;code&gt;count-1000-results&lt;/code&gt; output with &lt;code&gt;inspect-results&lt;/code&gt; from below would accomplish just that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inspect-results&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherQuery&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
    &lt;span class="s"&gt;MATCH (a)-[:div_by_ten]-&amp;gt;(b)-[:div_by_ten]-&amp;gt;(c)&lt;/span&gt;
    &lt;span class="s"&gt;WHERE id(c) = $that.data.id&lt;/span&gt;
    &lt;span class="s"&gt;RETURN a.i, a.prop, b.i, b.prop c.i, c.prop&lt;/span&gt;
  &lt;span class="na"&gt;andThen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PrintToStandardOut&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outputs stage of a standing query is where you can express your business logic and put Quine to work for you in your data pipeline. Take some time to review all of the possible output types in our &lt;a href="https://docs.quine.io/reference/rest-api.html#/schemas/StandingQueryResultOutput" rel="noopener noreferrer"&gt;API documentation&lt;/a&gt; located on &lt;a href="https://quine.io" rel="noopener noreferrer"&gt;https://quine.io&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Modifying standing queries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Modify a Standing Query Output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another time that you need to notify Quine of changes in your standing queries is when you modify the &lt;code&gt;outputs&lt;/code&gt; section of an existing standing query. The Quine API has two methods for the &lt;code&gt;/api/v1/query/standing/{standing-query-name}/output/{standing-query-output-name}&lt;/code&gt; endpoint that allow you to &lt;code&gt;DELETE&lt;/code&gt; and &lt;code&gt;POST&lt;/code&gt; a new output to an existing standing query. &lt;/p&gt;

&lt;p&gt;From above, let's change the original standing query output type from &lt;code&gt;Drop&lt;/code&gt; to a new &lt;code&gt;CypherQuery&lt;/code&gt; that outputs the matches to the console. We will use two API calls to accomplish the change. &lt;/p&gt;

&lt;p&gt;Delete the existing output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://0.0.0.0:8080/api/v1/query/standing/STANDING-1/output/count-1000-results &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the new output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://0.0.0.0:8080/api/v1/query/standing/STANDING-1/output/inspect-results &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "type": "CypherQuery",
    "query": "MATCH (a)-[:div_by_ten]-&amp;gt;(b)-[:div_by_ten]-&amp;gt;(c)\nWHERE id(c) = $that.data.id\nRETURN a.i, a.prop, b.i, b.prop c.i, c.prop",
    "andThen": {
      "type": "PrintToStandardOut"
    }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Propagate a New Standing Query&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a new standing query is registered in the system, it gets automatically registered only new nodes (or old nodes that are loaded back into the cache). This behavior is the default because pro-actively setting the standing query on all existing data might be quite costly depending on how much historical data there is. So Quine defaults to the most efficient option.&lt;/p&gt;

&lt;p&gt;However, sometimes there is a need to actively propagate standing queries across all previously ingested data as well. You can use the API to request that Quine propagate a new standing query to all nodes in the existing graph. Here's how the request looks in &lt;code&gt;curl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://0.0.0.0:8080/api/v1/query/standing/control/propagate?include-sleeping&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the in-product API documentation via the Quine web interface for additional code snippets. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this blog post, we looked at the different types of standing queries that you can create in Quine. A standing query is a powerful tool for data processing because it allows you to express your business logic as part of your data pipeline. We also looked at how you can modify an existing standing query output type and propagate a new standing query across the graph.&lt;/p&gt;

&lt;p&gt;Quine is open source if you want to explore standing queries for yourself using your own data. Download a precompiled version or build it yourself from the codebase from the &lt;a href="https://github.com/thatdot/quine" rel="noopener noreferrer"&gt;Quine Github&lt;/a&gt; codebase.&lt;/p&gt;

&lt;p&gt;Have a question, suggestion, or improvement? I welcome your feedback! Please drop into &lt;a href="https://quine-io.slack.com/" rel="noopener noreferrer"&gt;Quine Slack&lt;/a&gt; and let me know. I'm always happy to discuss Quine or answer questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Research
&lt;/h2&gt;

&lt;p&gt;From: &lt;a href="https://drive.google.com/file/d/17uw36E3juptE2QEEwKt-WLRhdXLM9r_R/view?usp=sharing" rel="noopener noreferrer"&gt;https://drive.google.com/file/d/17uw36E3juptE2QEEwKt-WLRhdXLM9r_R/view?usp=sharing&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing Query
&lt;/h2&gt;

&lt;p&gt;Quine implements a facility for executing any query type as a “Standing Query” which is persisted in the graph in this fashion. When a query is issued as a Standing Query, it includes callback functions describing what to do in each of the four cases where a node: &lt;/p&gt;

&lt;p&gt;1) Initially matches the query &lt;br&gt;
2) Initially does not match the query &lt;br&gt;
3) Initially did not match, but the node data changed so that it now does match the query &lt;br&gt;
4) Initially did match, but the node data changed so that it no longer matches the query &lt;/p&gt;

&lt;p&gt;To implement standing queries, Quine stores the result of each successive decomposition of a query (into smaller and smaller branches) on the node issuing that portion of the query. The query issued to the next node is essentially a subscription to the next nodes status as either matching the query, or not. Changes in the next node’s state result in a notification to the querying node. In this way, a complex query is relayed through the graph, where each node subscribes to whether the next node fulfills the smaller query. When a complete match is made, a special actor is notified with the results and the appropriate callback (established with the original query) is called. These callbacks can simply return results, but can also execute arbitrary functionality, like initiating new queries, or even rewriting parts of the graph when certain patterns are matched.&lt;/p&gt;




&lt;p&gt;From: Quine Innovations, Part II&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Execution
&lt;/h2&gt;

&lt;p&gt;Quine is implemented as a graph interpreter. A query is dropped into the graph and turned into a result by a recursive of process of: evaluate the first part of the query locally, and if it matches the requirements, relay the remainder to more nodes connected to the first (as relevant for the query definition), aggregating and processing the results returned. The relayed remainder of the query is smaller than the initial payload processed by the node in question. The process is repeated until the entire query is “consumed” and the relevant results returned and aggregated.&lt;/p&gt;

&lt;p&gt;The process of resolving a query happens in two directions: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the query is consumed while extending the remaining query parts to the next relevant set of nodes, &lt;/li&gt;
&lt;li&gt;results are returned from all relevant participating nodes in the query. The first component of query resolution is about exploration of the existing data graph to determine which nodes are responsible for resolving which portion of the query. The second component focuses on how results are returned to the requesting node recursively to produce the full and final result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Quine enables the unique opportunity to perform these steps separately so that the result of performing the first part (exploration) can result in a back-pressured stream of results delivered only when the consumer is ready to consume each of the next results. This allows the Quine system to achieve maximal memory and computational efficiency by computing a “recipe for results” in phase 1 which does not execute until the optimal moment when a consumer is ready to receive the results in phase 2. The back-pressure technique to slow a stream of data processing is well-know in the streaming data community; however the application of it in a granular node-by-node fashion when resolving graph queries is a novel innovation.&lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>streaminganalytics</category>
    </item>
    <item>
      <title>Real-time Graph Analytics for Kafka Streams with Quine</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Mon, 27 Jun 2022 13:55:46 +0000</pubDate>
      <link>https://dev.to/maglietti/real-time-graph-analytics-for-kafka-streams-with-quine-44cp</link>
      <guid>https://dev.to/maglietti/real-time-graph-analytics-for-kafka-streams-with-quine-44cp</guid>
      <description>&lt;p&gt;Kafka is the tool of choice by data engineers when building a streaming data pipeline. Adding Quine into a Kafka-centric data pipeline is the perfect way to introduce streaming analytics to the mix. Adding business logic directly into an event pipeline allows you to process high-value insights in real-time. &lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Streaming Pipeline
&lt;/h2&gt;

&lt;p&gt;Consider this straightforward, minimum viable streaming pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbnfbmgx52o06i0j2dk8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbnfbmgx52o06i0j2dk8o.png" alt="A simple streaming pipeline with Quine ingesting Kafka streaming data" width="800" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this simple pipeline, &lt;a href="https://vector.dev/" rel="noopener noreferrer"&gt;Vector&lt;/a&gt; will produce events, &lt;code&gt;dummy_log&lt;/code&gt; lines, once a second and stream them into a Kafka topic, &lt;code&gt;demo-logs&lt;/code&gt;, where an ingest stream from Quine will transform the log events into a streaming graph. &lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Vector
&lt;/h2&gt;

&lt;p&gt;Start by installing Vector in your environment. My examples use macOS and may need slight modifications to work correctly in your environment. I installed Vector with &lt;code&gt;brew install vector&lt;/code&gt;, which includes a sample &lt;code&gt;Vector.toml&lt;/code&gt; config in &lt;code&gt;/opt/homebrew/etc/vector&lt;/code&gt;. I extended the sample Vector config to build our pipeline. &lt;/p&gt;

&lt;p&gt;Run Vector to get a feel for the events that Vector emits. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;❯ vector -c /opt/homebrew/etc/vector/vector.toml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Vector generates dummy log lines from a built-in &lt;a href="https://vector.dev/docs/reference/configuration/sources/demo_logs/" rel="noopener noreferrer"&gt;demo_logs&lt;/a&gt; source. The log lines are transformed in Vector using the &lt;a href="https://vector.dev/docs/reference/vrl/functions/#parse_syslog" rel="noopener noreferrer"&gt;parse_syslog&lt;/a&gt; and emit a JSON object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"appname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Karimmove"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"facility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lpr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"some.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Take a breath, let it go, walk away"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"msgid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ID416"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"procid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9207&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-06-14T15:34:11.936Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Vector is emitting log entries, we need to connect that output to Kafka by adding in a &lt;a href="https://vector.dev/docs/reference/configuration/sinks/kafka/" rel="noopener noreferrer"&gt;Kafka sink&lt;/a&gt; element into the &lt;a href="https://gist.github.com/maglietti/abc26bb47c40940fb0b47ed37bed2c85" rel="noopener noreferrer"&gt;Vector.toml&lt;/a&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Stream parsed logs to kafka&lt;/span&gt;
&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;sinks.to_kafka&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;type = "kafka"&lt;/span&gt;
&lt;span class="s"&gt;inputs = [ "parse_logs" ]&lt;/span&gt;
&lt;span class="s"&gt;bootstrap_servers = "127.0.0.1:9092"&lt;/span&gt;
&lt;span class="s"&gt;key_field = "quine"&lt;/span&gt;
&lt;span class="s"&gt;topic = "demo-logs"&lt;/span&gt;
&lt;span class="s"&gt;encoding = "json"&lt;/span&gt;
&lt;span class="s"&gt;compression = "none"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Local Kafka Instance
&lt;/h2&gt;

&lt;p&gt;Kafka is the next step in the pipeline. I set up a single node Kafka cluster in Docker. There are more than enough examples on the internet of how to set up a Kafka cluster in Docker, and please set up the cluster in a way that fits your environment. My cluster uses a &lt;a href="https://gist.github.com/maglietti/03c09030feae1329950a3a1db2ed8fd8" rel="noopener noreferrer"&gt;docker-compose&lt;/a&gt; file that launches version 7.1.1 of Zookeeper and Kafka containers. &lt;/p&gt;

&lt;p&gt;Start the Kafka cluster and create a topic called &lt;code&gt;demo-logs&lt;/code&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note, I had to run the &lt;code&gt;docker compose up&lt;/code&gt; command a couple of times before both the Zookeeper and Kafka containers launched cleanly. Make sure the containers fully load at least once before including the &lt;code&gt;-d&lt;/code&gt; option to run them in detached mode.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ Docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
❯ docker &lt;span class="nb"&gt;exec &lt;/span&gt;Kafka Kafka-topics &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; kafka:9092 &lt;span class="nt"&gt;--create&lt;/span&gt; &lt;span class="nt"&gt;--topic&lt;/span&gt; demo-logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;a href="https://github.com/edenhill/kcat" rel="noopener noreferrer"&gt;kcat&lt;/a&gt; to verify the Kafka cluster is up and that the &lt;code&gt;demo-logs&lt;/code&gt; topic was configured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ kcat &lt;span class="nt"&gt;-b&lt;/span&gt; localhost &lt;span class="nt"&gt;-L&lt;/span&gt;
Metadata &lt;span class="k"&gt;for &lt;/span&gt;all topics &lt;span class="o"&gt;(&lt;/span&gt;from broker &lt;span class="nt"&gt;-1&lt;/span&gt;: localhost:9092/bootstrap&lt;span class="o"&gt;)&lt;/span&gt;:
 1 brokers:
  broker 1 at 127.0.0.1:9092 &lt;span class="o"&gt;(&lt;/span&gt;controller&lt;span class="o"&gt;)&lt;/span&gt;
 1 topics:
  topic &lt;span class="s2"&gt;"demo-logs"&lt;/span&gt; with 1 partitions:
    partition 0, leader 1, replicas: 1, isrs: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quine Config
&lt;/h2&gt;

&lt;p&gt;Ok, let's get Quine configured and ready to receive the log events from Kafka via an ingest stream. We can start with a simple ingest stream that takes each demo log line and creates a node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ingestStreams&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KafkaIngest&lt;/span&gt;
    &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;demo-logs&lt;/span&gt;
    &lt;span class="na"&gt;bootstrapServers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:9092&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherJson&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (n)&lt;/span&gt;
        &lt;span class="s"&gt;WHERE id(n) = idFrom($that)&lt;/span&gt;
        &lt;span class="s"&gt;SET n.line = $that&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Launch the Pipeline
&lt;/h2&gt;

&lt;p&gt;Let's launch Vector and Quine to get the pipeline moving. &lt;/p&gt;

&lt;p&gt;Launch Vector using the modified &lt;a href="https://gist.github.com/maglietti/abc26bb47c40940fb0b47ed37bed2c85" rel="noopener noreferrer"&gt;vector.toml&lt;/a&gt; configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ vector &lt;span class="nt"&gt;-c&lt;/span&gt; vector.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch Quine by running the &lt;a href=""&gt;Kafka Pipeline&lt;/a&gt; recipe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ java &lt;span class="nt"&gt;-jar&lt;/span&gt; quine-x.x.x &lt;span class="nt"&gt;-r&lt;/span&gt; kafka_pipeline.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And verify that we see nodes generated in Quine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Quine app web server available at http://0.0.0.0:8080

 | &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; INGEST-1 status is running and ingested 18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations! 🎉 Your pipeline is operating!&lt;/p&gt;

&lt;h2&gt;
  
  
  Improving the Ingest Query
&lt;/h2&gt;

&lt;p&gt;The ingest query that I started with is pretty basic. Using &lt;code&gt;CALL recentNodes(1)&lt;/code&gt;, let's take a look at the newest node in the graph and see what the query produced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ &lt;span class="c"&gt;## Get Latest Node&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="s2"&gt;"http://0.0.0.0:8080/api/v1/query/cypher"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: text/plain'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"CALL recentNodes(1)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"columns"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"node"&lt;/span&gt;
  &lt;span class="o"&gt;]&lt;/span&gt;,
  &lt;span class="s2"&gt;"results"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"9fde7ef4-c5ec-35f1-ae5f-619bd9ab7d5c"&lt;/span&gt;,
        &lt;span class="s2"&gt;"labels"&lt;/span&gt;: &lt;span class="o"&gt;[]&lt;/span&gt;,
        &lt;span class="s2"&gt;"properties"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"line"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"appname"&lt;/span&gt;: &lt;span class="s2"&gt;"benefritz"&lt;/span&gt;,
            &lt;span class="s2"&gt;"facility"&lt;/span&gt;: &lt;span class="s2"&gt;"uucp"&lt;/span&gt;,
            &lt;span class="s2"&gt;"hostname"&lt;/span&gt;: &lt;span class="s2"&gt;"make.de"&lt;/span&gt;,
            &lt;span class="s2"&gt;"message"&lt;/span&gt;: &lt;span class="s2"&gt;"#hugops to everyone who has to deal with this"&lt;/span&gt;,
            &lt;span class="s2"&gt;"msgid"&lt;/span&gt;: &lt;span class="s2"&gt;"ID873"&lt;/span&gt;,
            &lt;span class="s2"&gt;"procid"&lt;/span&gt;: 871,
            &lt;span class="s2"&gt;"severity"&lt;/span&gt;: &lt;span class="s2"&gt;"emerg"&lt;/span&gt;,
            &lt;span class="s2"&gt;"timestamp"&lt;/span&gt;: &lt;span class="s2"&gt;"2022-06-14T19:58:16.463Z"&lt;/span&gt;,
            &lt;span class="s2"&gt;"version"&lt;/span&gt;: 1
          &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ingest query creates nodes using &lt;code&gt;idFrom()&lt;/code&gt;, populated them with the properties that it received from Kafka, and didn't create any relationships. We can make this node more useful by giving it a label and removing parameters that are not interesting to us. Additionally, using &lt;code&gt;reify.time()&lt;/code&gt;, I can associate the node with a &lt;code&gt;timeNode&lt;/code&gt; to stitch together events that occur across the network in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing the sample data
&lt;/h2&gt;

&lt;p&gt;Quine has a web-based graph explorer that really comes to life once you have a handle on the shape of the streaming data. But I am starting from the beginning with a bare-bones recipe. For me, when I start pulling apart a stream of data, I find that using the API to ask a few analytical questions serves me well. &lt;/p&gt;

&lt;p&gt;I'll use the &lt;code&gt;/query/cypher&lt;/code&gt; endpoint to get a feel for the shape of the sample data streaming from Kafka. I don't recommend doing a full node scan on a mature streaming graph, but my streaming graph is still young and small. &lt;/p&gt;

&lt;p&gt;Using my REST API client of choice, I POST a Cypher query that returns the metrics (counts) for parameters that are interesting. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1rlnkhj67hlmt8g1sou2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1rlnkhj67hlmt8g1sou2.png" alt="Paw Screenshot" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a lot of JSON results to review; let's take this over to a Jupyter Notebook to continue the analysis. My REST API client includes a Python snip-it tool that makes it really easy to move directly into code without having to start from scratch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flhpbap41lnwmh6zk6elx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flhpbap41lnwmh6zk6elx.png" alt="Jupyter Screenshot" width="800" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://gist.github.com/maglietti/4fdbc681d703490811a8988e16b08b3f" rel="noopener noreferrer"&gt;Jupyter&lt;/a&gt;, within a few cells, I had the JSON response data loaded into a Pandas DataFrame and an easy to review textual visualization of what the sample data contains. &lt;/p&gt;

&lt;p&gt;I let the pipeline run while I developed simple visualizations of the metrics. Right away, I could see that the sample data Vector produces is random and uniformly distributed across all of the parameters in the graph. And after 15000 log lines, the sample generation exhausted all permutations of the data. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdp7d2uwvloobhtksani.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdp7d2uwvloobhtksani.png" alt="Data Analysis" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and Next Steps
&lt;/h2&gt;

&lt;p&gt;I learned a lot about streaming data while setting up this pipeline. Vector is a great tool that allows you to stream log files into Kafka for analysis. Add a Quine instance on the other side of Kafka, and you are able to perform streaming analytics inside a streaming graph using standing queries. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the same workflow to develop an understanding of streaming data that you do for data at rest&lt;/li&gt;
&lt;li&gt;Perform streaming analysis by connecting Quine to your Kafka cluster&lt;/li&gt;
&lt;li&gt;Use Cypher ingest queries to form the graph within a Quine ingest stream. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quine is open source if you want to run this analysis for yourself. Download a precompiled version or build it yourself from the codebase (&lt;a href="https://github.com/thatdot/quine" rel="noopener noreferrer"&gt;Quine Github&lt;/a&gt;). I published the recipe that I developed at &lt;code&gt;https://quine.io/recipes&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Have a question, suggestion, or improvement? I welcome your feedback! Please drop into &lt;a href="https://quine-io.slack.com/" rel="noopener noreferrer"&gt;Quine Slack&lt;/a&gt; and let me know. I'm always happy to discuss Quine or answer questions. &lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>datamodel</category>
      <category>kafka</category>
    </item>
    <item>
      <title>Processing Machine Logs with Streaming Graph</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Thu, 16 Jun 2022 14:15:50 +0000</pubDate>
      <link>https://dev.to/maglietti/processing-machine-logs-with-streaming-graph-7b7</link>
      <guid>https://dev.to/maglietti/processing-machine-logs-with-streaming-graph-7b7</guid>
      <description>&lt;p&gt;You know we had to get here eventually. I'm looking into all of the ways that Quine can connect to and ingest streaming sources. Next up is my old friend, the log file.&lt;/p&gt;

&lt;p&gt;Log files are a structured stream of parsable data using regular expressions. Log lines are emitted at all levels of an application. The challenge is that they are primarily islands of disconnected bits of the overall picture. Placed into a data pipeline, Quine allows us to combine different types of logs and use a standing query to match interesting patterns upstream of a log analytics solution like Splunk or Sumo Logic. &lt;/p&gt;

&lt;h2&gt;
  
  
  Log Line Structure
&lt;/h2&gt;

&lt;p&gt;Processing log files can quickly become as messy as the log files themself. I think that it's best to approach a log file  like any other data source and take the time to understand the log line structure before asking any questions.  &lt;/p&gt;

&lt;p&gt;Quine is an application that produces log lines, and just like many other applications, the structure of the log lines follows a pattern. The logline pattern is defined in &lt;a href="https://github.com/thatdot/quine/blob/main/quine-core/src/main/resources/logging.conf#L11" rel="noopener noreferrer"&gt;Scala&lt;/a&gt;, making it very easy for us to understand what the log line contains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"%date %level [%mdc{akkaSource:-NotFromActor}] [%thread] %logger - %msg%n%ex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quine Log RegEx
&lt;/h2&gt;

&lt;p&gt;Each Quine log line was assembled using the pre-defined pattern. This presents a perfect opportunity to use a regular expression, reverse the pattern, and build a streaming graph. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note, the regex link in the example below uses the log output from a Quine Enterprise cluster. Learn more about the &lt;a href="https://www.thatdot.com/product/pricing" rel="noopener noreferrer"&gt;Quine Enterprise&lt;/a&gt; and other &lt;a href="https://www.thatdot.com/product/resources" rel="noopener noreferrer"&gt;products&lt;/a&gt; created by thatDot on our &lt;a href="https://www.thatdot.com/" rel="noopener noreferrer"&gt;web site&lt;/a&gt;. The regular expression will work for both Quine and Quine Enterprise. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I developed a &lt;a href="https://regex101.com/r/02qYsJ/3" rel="noopener noreferrer"&gt;regular expression&lt;/a&gt; that reverses the logline and returns the log elements for use by the ingest stream ingest query. I also published a recipe that uses the regular expression to parse Quine log lines on Quine.io.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(^&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;}-&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;}-&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;}:&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;}:&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;},&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="nf"&gt;string&lt;/span&gt; 
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;FATAL&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="no"&gt;ERROR&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="no"&gt;WARN&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="no"&gt;INFO&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="no"&gt;DEBUG&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;[(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="no"&gt;S&lt;/span&gt;&lt;span class="o"&gt;*)&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;                                      &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;[(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="no"&gt;S&lt;/span&gt;&lt;span class="o"&gt;*)&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;                                      &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="nf"&gt;name&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="no"&gt;S&lt;/span&gt;&lt;span class="o"&gt;*)&lt;/span&gt;                                          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt;                                              &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;the&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;
&lt;span class="o"&gt;((?:(?!^[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&gt;]{&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;}(?:-[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&gt;]{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;}){&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;}(?:[^|&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;]+){&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;}).*(?:&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;)?)+)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quine Log Ingest Stream
&lt;/h2&gt;

&lt;p&gt;In my previous article, I connected to a &lt;code&gt;CSV&lt;/code&gt; file using the &lt;code&gt;CypherCsv&lt;/code&gt; &lt;code&gt;FileIngest&lt;/code&gt; format so that Quine could break the rows of data stored in the file back into columns. The &lt;code&gt;CypherLine&lt;/code&gt; &lt;code&gt;FileIngest&lt;/code&gt; format allows us to read each line into the &lt;code&gt;$that&lt;/code&gt; variable and process it through a Cypher query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ingestStreams&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$in_file&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherLine&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
        &lt;span class="s"&gt;// Quine log pattern "%date %level [%mdc{akkaSource:-NotFromActor}] [%thread] %logger - %msg%n%ex"&lt;/span&gt;
        &lt;span class="s"&gt;WITH text.regexFirstMatch($that, "(^\\d{4}-\\d{2}-\\d{2} \\d{1,2}:\\d{2}:\\d{2},\\d{3}) (FATAL|ERROR|WARN|INFO|DEBUG) \\[(\\S*)\\] \\[(\\S*)\\] (\\S*) - (.*)") as r &lt;/span&gt;
        &lt;span class="s"&gt;WHERE r IS NOT NULL &lt;/span&gt;
        &lt;span class="s"&gt;// 0: whole matched line&lt;/span&gt;
        &lt;span class="s"&gt;// 1: date time string&lt;/span&gt;
        &lt;span class="s"&gt;// 2: log level&lt;/span&gt;
        &lt;span class="s"&gt;// 3: actor address. Might be inside of `akka.stream.Log(…)`&lt;/span&gt;
        &lt;span class="s"&gt;// 4: thread name&lt;/span&gt;
        &lt;span class="s"&gt;// 5: logging class&lt;/span&gt;
        &lt;span class="s"&gt;// 6: Message&lt;/span&gt;
        &lt;span class="s"&gt;WITH *, split(r[3], "/") as path, split(r[6], "(") as msgPts&lt;/span&gt;
        &lt;span class="s"&gt;WITH *, replace(COALESCE(split(path[2], "@")[-1], 'No host'),")","") as qh&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (actor), (msg), (class), (host)&lt;/span&gt;
        &lt;span class="s"&gt;WHERE id(host)  = idFrom("host", qh)&lt;/span&gt;
          &lt;span class="s"&gt;AND id(actor) = idFrom("actor", r[3])&lt;/span&gt;
          &lt;span class="s"&gt;AND id(msg)   = idFrom("msg", r[0])&lt;/span&gt;
          &lt;span class="s"&gt;AND id(class) = idFrom("class", r[5])&lt;/span&gt;
        &lt;span class="s"&gt;SET host: Host, host.address = split(qh, ":")[0], host.port = split(qh, ":")[-1], host.host = qh,&lt;/span&gt;
            &lt;span class="s"&gt;actor: Actor, actor.address = r[3], actor.id = replace(path[-1],")",""), actor.shard = path[-2], actor.type = path[-3],&lt;/span&gt;
            &lt;span class="s"&gt;msg: Message, msg.msg = r[6], msg.type = split(msgPts[0], " ")[0], msg.level = r[2],&lt;/span&gt;
            &lt;span class="s"&gt;class: Class, class.class = r[5]&lt;/span&gt;
        &lt;span class="s"&gt;WITH * CALL reify.time(datetime({date: localdatetime(r[1], "yyyy-MM-dd HH:mm:ss,SSS")})) YIELD node AS time&lt;/span&gt;
        &lt;span class="s"&gt;CREATE (actor)-[:sent]-&amp;gt;(msg),&lt;/span&gt;
               &lt;span class="s"&gt;(actor)-[:of_class]-&amp;gt;(class),&lt;/span&gt;
               &lt;span class="s"&gt;(actor)-[:on_host]-&amp;gt;(host),&lt;/span&gt;
               &lt;span class="s"&gt;(msg)-[:at_time]-&amp;gt;(time)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ingest stream definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads Quine log lines from a file&lt;/li&gt;
&lt;li&gt;Parses each line with regex&lt;/li&gt;
&lt;li&gt;Creates host, actor, message, and class nodes&lt;/li&gt;
&lt;li&gt;Populates the node properties&lt;/li&gt;
&lt;li&gt;Relates the nodes in the streaming graph&lt;/li&gt;
&lt;li&gt;Anchors the message with a relationship to a time node from &lt;code&gt;reify.time&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring Quine Logs
&lt;/h2&gt;

&lt;p&gt;Ok, let's run this recipe and see how it works. By default, the log level in Quine is set to &lt;code&gt;WARN&lt;/code&gt;. We can increase the log level in the configuration or pass in a Java system configuration property when we launch Quine. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Set the log level in Quine (or Quine Enterprise) via the &lt;code&gt;thatdot.loglevel&lt;/code&gt; configuration option. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Setting Log Level in Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start by getting your current Quine configuration. The easiest way to get the configuration is to start Quine and then &lt;code&gt;GET&lt;/code&gt; the configuration via an API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://0.0.0.0:8080/api/v1/admin/config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; quine.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit the &lt;code&gt;quine.conf&lt;/code&gt; file and add &lt;code&gt;"thatdot":{"loglevel":"DEBUG"},&lt;/code&gt; before the &lt;code&gt;quine&lt;/code&gt; object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ jq &lt;span class="s1"&gt;'.'&lt;/span&gt; quine.conf
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"thatdot"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"loglevel"&lt;/span&gt;: &lt;span class="s2"&gt;"DEBUG"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"quine"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"decline-sleep-when-access-within"&lt;/span&gt;: &lt;span class="s2"&gt;"0"&lt;/span&gt;,
    &lt;span class="s2"&gt;"decline-sleep-when-write-within"&lt;/span&gt;: &lt;span class="s2"&gt;"100ms"&lt;/span&gt;,
    &lt;span class="s2"&gt;"dump-config"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
    &lt;span class="s2"&gt;"edge-iteration"&lt;/span&gt;: &lt;span class="s2"&gt;"reverse-insertion"&lt;/span&gt;,
    &lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"partitioned"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
      &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"uuid"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"in-memory-hard-node-limit"&lt;/span&gt;: 75000,
    &lt;span class="s2"&gt;"in-memory-soft-node-limit"&lt;/span&gt;: 10000,
    &lt;span class="s2"&gt;"labels-property"&lt;/span&gt;: &lt;span class="s2"&gt;"__LABEL"&lt;/span&gt;,
    &lt;span class="s2"&gt;"metrics-reporters"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"jmx"&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"persistence"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"effect-order"&lt;/span&gt;: &lt;span class="s2"&gt;"memory-first"&lt;/span&gt;,
      &lt;span class="s2"&gt;"journal-enabled"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
      &lt;span class="s2"&gt;"snapshot-schedule"&lt;/span&gt;: &lt;span class="s2"&gt;"on-node-sleep"&lt;/span&gt;,
      &lt;span class="s2"&gt;"snapshot-singleton"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
      &lt;span class="s2"&gt;"standing-query-schedule"&lt;/span&gt;: &lt;span class="s2"&gt;"on-node-sleep"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"shard-count"&lt;/span&gt;: 4,
    &lt;span class="s2"&gt;"should-resume-ingest"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
    &lt;span class="s2"&gt;"store"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"create-parent-dir"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
      &lt;span class="s2"&gt;"filepath"&lt;/span&gt;: &lt;span class="s2"&gt;"quine.db"&lt;/span&gt;,
      &lt;span class="s2"&gt;"sync-all-writes"&lt;/span&gt;: &lt;span class="nb"&gt;false&lt;/span&gt;,
      &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"rocks-db"&lt;/span&gt;,
      &lt;span class="s2"&gt;"write-ahead-log"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"timeout"&lt;/span&gt;: &lt;span class="s2"&gt;"2m"&lt;/span&gt;,
    &lt;span class="s2"&gt;"webserver"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"address"&lt;/span&gt;: &lt;span class="s2"&gt;"0.0.0.0"&lt;/span&gt;,
      &lt;span class="s2"&gt;"enabled"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
      &lt;span class="s2"&gt;"port"&lt;/span&gt;: 8080
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, restart Quine and include the &lt;code&gt;config.file&lt;/code&gt; property.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-Dconfig&lt;/span&gt;.file&lt;span class="o"&gt;=&lt;/span&gt;quine.conf &lt;span class="nt"&gt;-jar&lt;/span&gt; quine-x.x.x.jar &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; quineLog.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DEBUG&lt;/code&gt; level log lines will stream into the  &lt;code&gt;quineLog.log&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passing Log Level at Runtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another slightly more straightforward way to enable Quine logs is to pass in a Java system configuration property. Here's how to start Quine and enable logging from the command line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-Dthatdot&lt;/span&gt;.loglevel&lt;span class="o"&gt;=&lt;/span&gt;DEBUG &lt;span class="nt"&gt;-jar&lt;/span&gt; quine-x.x.x.jar &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; quineLog.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DEBUG&lt;/code&gt; level log lines will stream into the &lt;code&gt;quineLog.log&lt;/code&gt; file. &lt;/p&gt;

&lt;h2&gt;
  
  
  Ingesting Other Log Formats
&lt;/h2&gt;

&lt;p&gt;You can easily modify the regex I developed for Quine log lines above to parse similar log output, like those found in *nix based system files or other Java applications. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard-ish Java Log Output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depending on the &lt;code&gt;log level&lt;/code&gt;, Java emits a lot of information into logs. This ingest stream handles application log lines from most Java applications. Sometimes the log entry itself spans multiple lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$app_log&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherJson&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;WITH *, text.regexFirstMatch($that.message, '^(\\d{4}(?:-\\d{2}){2}(?:[^]\\r?\\n]+))\\s+?\\[(.+?)\\]\\s+?(\\S+?)\\s+(.+?)\\s+\\-\\s+((?:(?!^\\d{4}(?:-\\d{2}){2}(?:[^|\\r?\\n]+){3}).*(?:\\r?\\n)?)+)') AS r WHERE r IS NOT NULL&lt;/span&gt;
      &lt;span class="s"&gt;CREATE (log {&lt;/span&gt;
        &lt;span class="s"&gt;timestamp: r[1],&lt;/span&gt;
        &lt;span class="s"&gt;component: r[2],&lt;/span&gt;
        &lt;span class="s"&gt;level: r[3],&lt;/span&gt;
        &lt;span class="s"&gt;subprocess: r[4],&lt;/span&gt;
        &lt;span class="s"&gt;message: r[5],&lt;/span&gt;
        &lt;span class="s"&gt;type: 'log'&lt;/span&gt;
      &lt;span class="s"&gt;})&lt;/span&gt;
      &lt;span class="s"&gt;// Create hour/minute buckets per event&lt;/span&gt;
      &lt;span class="s"&gt;WITH * WHERE r[1] IS NOT NULL CALL reify.time(datetime({date: localdatetime(r[1], "yyyy-MM-dd HH:mm:ss,SSS")}), ["hour","minute"]) YIELD node AS timeNode&lt;/span&gt;
      &lt;span class="s"&gt;// Create edges for timenNodes&lt;/span&gt;
      &lt;span class="s"&gt;CREATE (log)-[:at]-&amp;gt;(timeNode)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ubuntu Ubuntu 22.04 LTS Syslog&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're developing distributed applications, you will most likely need a regular expression that parses the Ubuntu &lt;code&gt;/var/log/syslog&lt;/code&gt; file. First, you need to edit &lt;code&gt;/etc/rsyslog.conf&lt;/code&gt; and uncomment the line to emit the traditional &lt;code&gt;DateTime&lt;/code&gt; format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;#
# Use traditional timestamp format.
# To enable high precision timestamps, comment out the following line.
#
&lt;/span&gt;&lt;span class="err"&gt;$ActionFileDefaultTemplate&lt;/span&gt; &lt;span class="err"&gt;RSYSLOG_TraditionalFileFormat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log line format is:&lt;br&gt;
&lt;code&gt;%timestamp:::date-rfc3339% %HOSTNAME% %app-name% %procid% %msgid% %msg%n&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$syslog&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherLine&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;WITH text.regexFirstMatch($that, '^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d*?\\+\\d{2}:\\d{2}|Z).?\\s(.*?)(?=\\s).?\\s(\\S+)\\[(\\S+)\\]:\\s(.*)') AS s WHERE s IS NOT NULL&lt;/span&gt;
      &lt;span class="s"&gt;CREATE (syslog {&lt;/span&gt;
        &lt;span class="s"&gt;timestamp: s[1],&lt;/span&gt;
        &lt;span class="s"&gt;hostname: s[2],&lt;/span&gt;
        &lt;span class="s"&gt;app_name: s[3],&lt;/span&gt;
        &lt;span class="s"&gt;proc_id: s[4],&lt;/span&gt;
        &lt;span class="s"&gt;message: s[5],&lt;/span&gt;
        &lt;span class="s"&gt;type: 'syslog'&lt;/span&gt;
      &lt;span class="s"&gt;})&lt;/span&gt;
      &lt;span class="s"&gt;// Create hour/minute buckets per event&lt;/span&gt;
      &lt;span class="s"&gt;WITH * WHERE s[1] IS NOT NULL CALL reify.time(datetime({date: localdatetime(s[1], "yyyy-MM-dd'T'HH:mm:ss.SSSSSSz")}), ["hour","minute"]) YIELD node AS timeNode&lt;/span&gt;
      &lt;span class="s"&gt;// Create edges for timenNodes&lt;/span&gt;
      &lt;span class="s"&gt;CREATE (syslog)-[:at]-&amp;gt;(timeNode)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MySQL Error Log&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Working on a web application that's been around for a while, it's probably sitting on top of a MySQL database. The traditional-format MySQL log messages have these &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/error-log-format.html" rel="noopener noreferrer"&gt;fields&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;time thread [label] [err_code] [subsystem] msg&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;2022-04-14T06:55:26.961757Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: /var/run/mysqld/mysqlx.sock&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add these log entries to your streaming graph for analysis too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FileIngest&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$sqlerr_log&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherLine&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;WITH text.regexFirstMatch($that, '^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{6}Z)\\s(\\d)\\s\\[(\\S+)\\]\\s\\[(\\S+)\\]\\s\\[(\\S+)\\]\\s(.*)') AS m WHERE m IS NOT NULL&lt;/span&gt;
      &lt;span class="s"&gt;CREATE (sqllog {&lt;/span&gt;
        &lt;span class="s"&gt;timestamp: m[1],&lt;/span&gt;
        &lt;span class="s"&gt;thread: m[2],&lt;/span&gt;
        &lt;span class="s"&gt;label: m[3],&lt;/span&gt;
        &lt;span class="s"&gt;err_code: m[4],&lt;/span&gt;
        &lt;span class="s"&gt;subsystem: m[5],&lt;/span&gt;
        &lt;span class="s"&gt;message: m[6],&lt;/span&gt;
        &lt;span class="s"&gt;type: 'sqllog'&lt;/span&gt;
      &lt;span class="s"&gt;})&lt;/span&gt;
      &lt;span class="s"&gt;// Create hour/minute buckets per event&lt;/span&gt;
      &lt;span class="s"&gt;WITH * WHERE m[1] IS NOT NULL CALL reify.time(datetime({date: localdatetime(m[1], "yyyy-MM-dd'T'HH:mm:ss.SSSSSSz")}), ["hour","minute"]) YIELD node AS timeNode&lt;/span&gt;
      &lt;span class="s"&gt;// Create edges for timenNodes&lt;/span&gt;
      &lt;span class="s"&gt;CREATE (sqllog)-[:at]-&amp;gt;(timeNode)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Streaming data comes from all kinds of sources. With Quine, it's easy to convert that data stream into a streaming graph. &lt;/p&gt;

&lt;p&gt;Quine is open source if you want to run this analysis for yourself. Download a precompiled version or build it yourself from the codebase (&lt;a href="https://github.com/thatdot/quine" rel="noopener noreferrer"&gt;Quine Github&lt;/a&gt;). I published the recipe that I developed at &lt;code&gt;https://quine.io/recipes&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Have a question, suggestion, or improvement? I welcome your feedback! Please drop in to &lt;a href="https://quine-io.slack.com/" rel="noopener noreferrer"&gt;Quine Slack&lt;/a&gt; and let me know. I'm always happy to discuss Quine or answer questions. &lt;/p&gt;




</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>datamodel</category>
    </item>
    <item>
      <title>Ingesting From Multiple Data Sources into Quine Streaming Graphs</title>
      <dc:creator>Michael Aglietti</dc:creator>
      <pubDate>Mon, 06 Jun 2022 18:12:57 +0000</pubDate>
      <link>https://dev.to/maglietti/ingesting-from-multiple-data-sources-into-quine-streaming-graphs-21m2</link>
      <guid>https://dev.to/maglietti/ingesting-from-multiple-data-sources-into-quine-streaming-graphs-21m2</guid>
      <description>&lt;p&gt;As part of the ongoing series in which I exploring different ways to use the ingest stream to load data into Quine, I want to cover one of Quine's specialities: building a streaming graph from multiple data sources. This time, we'll work with &lt;code&gt;CSV&lt;/code&gt; data exported from IMDb to answer the question; &lt;em&gt;"Which actors have acted in and directed the same movie?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;CSV&lt;/code&gt; Files
&lt;/h2&gt;

&lt;p&gt;Usually, if someone says that they have data, most likely it's going to be in &lt;code&gt;CSV&lt;/code&gt; format or pretty darn close to it. (Or &lt;code&gt;JSON&lt;/code&gt;, but that is another blog post.) In our case, we have two files filled with data in &lt;code&gt;CSV&lt;/code&gt; format. Let's inspect what's inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File 1: &lt;code&gt;movieData.csv&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;movieData.csv&lt;/code&gt; file contains records for actors, movies, and the actor's relationship to the movie. Conveniently, each record type has a schema, flattened into rows during export.  &lt;/p&gt;

&lt;p&gt;Should we separate the data back into discrete files and then load them? No, we can set up separate ingest streams to act on each data type in the file. Effectively, we will separate the "jobs to do" into Cypher queries and stream in the data. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File 2: &lt;code&gt;ratingData.csv&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our second file, &lt;code&gt;ratingData.csv&lt;/code&gt; is very straightforward. It contains 100,000 rows of movie ratings. Adding the &lt;code&gt;ratings&lt;/code&gt; data into our model completes our discovery phase for the supplied data. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvqftrx67iebnb3kcqdb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvvqftrx67iebnb3kcqdb.png" alt="Original implied schema of IMDB data." width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;CypherCsv&lt;/code&gt; Ingest Stream
&lt;/h2&gt;

&lt;p&gt;The Quine API documentation defines the &lt;a href="https://docs.quine.io/reference/rest-api.html#/schemas/com.thatdot.quine.routes.IngestStreamConfiguration" rel="noopener noreferrer"&gt;schema&lt;/a&gt; of the &lt;em&gt;File Ingest Format&lt;/em&gt; ingest stream for us. The schema is robust and accommodates &lt;code&gt;CSV&lt;/code&gt;, &lt;code&gt;JSON&lt;/code&gt;, and &lt;code&gt;line&lt;/code&gt; file types. Please take a moment to read through the documentation. Be sure to select &lt;code&gt;type: FileIngest&lt;/code&gt; -&amp;gt; &lt;code&gt;format: CypherCsv&lt;/code&gt; using the API documentation dropdowns. &lt;/p&gt;

&lt;p&gt;I define ingest streams to transform and load the movie data into Quine. Quine ingest streams behave independently and in parallel when processing files. This means that we can have multiple ingest streams operating on a single file. This is the case for the &lt;code&gt;movieData.csv&lt;/code&gt; file because there are several operations that we need to perform on multiple types of data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Movie Rows
&lt;/h2&gt;

&lt;p&gt;The first ingest stream that I set up will address the &lt;code&gt;Movie&lt;/code&gt; rows in the &lt;code&gt;movieData.csv&lt;/code&gt; file. There are 9125 movies in the data set. I create two nodes from each Movie row using an ingest query, &lt;code&gt;movie&lt;/code&gt; and &lt;code&gt;genre&lt;/code&gt;. I store all of the movie data as properties in the Movie mode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;$that&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;row.Entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Movie'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Movie"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.tmdbId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.imdbId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.imdbId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.imdbRating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toFloat&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.imdbRating&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.released&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.released&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.title&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.year&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.poster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.poster&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.runtime&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.countries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.countries&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"|"&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.imdbVotes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.imdbVotes&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.revenue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.revenue&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.plot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.plot&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.url&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;m.budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.budget&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.languages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.languages&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"|"&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;m.movieId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.genres&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"|"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;genres&lt;/span&gt;
&lt;span class="k"&gt;UNWIND&lt;/span&gt; &lt;span class="n"&gt;genres&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Genre"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;g.genre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;g:&lt;/span&gt;&lt;span class="n"&gt;Genre&lt;/span&gt;
&lt;span class="k"&gt;MERGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:IN_GENRE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;g:&lt;/span&gt;&lt;span class="n"&gt;Genre&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quine passes each line to the ingest stream via the variable &lt;code&gt;$that&lt;/code&gt; to which I assign the identity &lt;code&gt;row&lt;/code&gt;. A &lt;code&gt;MATCH&lt;/code&gt; is made when the &lt;code&gt;row.Entity&lt;/code&gt; value is &lt;code&gt;Movie&lt;/code&gt; and a node &lt;code&gt;id&lt;/code&gt; is returned from the &lt;code&gt;idFrom()&lt;/code&gt; function. &lt;code&gt;SET&lt;/code&gt; is used to give the node a label and to store metadata as node properties. &lt;/p&gt;

&lt;p&gt;Each movie row has a pipe &lt;code&gt;|&lt;/code&gt; delimited list of genres in the &lt;code&gt;genres&lt;/code&gt; column. I split the column value apart and created a Genre node for each genre in the list, labeled and containing the genre as a property. &lt;/p&gt;

&lt;p&gt;Finally, the &lt;code&gt;Movie&lt;/code&gt; node is related to the &lt;code&gt;Genre&lt;/code&gt; node with &lt;code&gt;MERGE&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Person Rows
&lt;/h2&gt;

&lt;p&gt;The second ingest stream addresses the &lt;code&gt;Person&lt;/code&gt; rows in the same way I did for the &lt;code&gt;Movie&lt;/code&gt; rows. There are 19047 person records in the &lt;code&gt;movieData.csv&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;$that&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;row.Entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Person"&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Person"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.imdbId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.imdbId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.bornIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.bornIn&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.bio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.bio&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.poster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.poster&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.url&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.born&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.born&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.died&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.died&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.tmdbId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.born&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;row.born&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.born&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"T00:00:00Z"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p.died&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;row.died&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.died&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"T00:00:00Z"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ingest query in this ingest stream matches when the &lt;code&gt;row.Entity&lt;/code&gt; is &lt;code&gt;Person&lt;/code&gt;, creates a node using the &lt;code&gt;idFrom()&lt;/code&gt; function, and stores the Person metadata in node parameters. &lt;/p&gt;

&lt;h2&gt;
  
  
  Join Rows
&lt;/h2&gt;

&lt;p&gt;Looking at the rows that have &lt;code&gt;Join&lt;/code&gt; in the &lt;code&gt;Entity&lt;/code&gt; column leads me to believe that the data in this &lt;code&gt;CSV&lt;/code&gt; file originated from a relational database. There are two types of joins in the file, &lt;code&gt;Acted&lt;/code&gt; and &lt;code&gt;Directed&lt;/code&gt;. The ingest queries below process them. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acted In&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;$that&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;row.Entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Join"&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;row.Work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Acting"&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Person"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Movie"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Role"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.role&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; 
  &lt;span class="n"&gt;r.role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.role&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;r.movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;r.tmdbId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; 
  &lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;
&lt;span class="k"&gt;MERGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PLAYED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_ROLE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MERGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ACTED_IN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Acted join rows create relationships between Person, Role, and Movie nodes. There are two paths created from the Person nodes. The first path &lt;code&gt;(p)-[:PLAYED]-&amp;gt;(r)&amp;lt;-[:HAS_ROLE]-(m)&lt;/code&gt; establishes the relationship between actors (Person) and the roles they have played as well as the roles in a movie (Movies). A second path is formed that directly relates an actor to movies they acted in. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directed&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;$that&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;row.Entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Join"&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;row.Work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Directing"&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Person"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.tmdbId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Movie"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MERGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DIRECTED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Directed ingest query matches join rows and creates a path relating directors with the movies they have directed. &lt;/p&gt;

&lt;h2&gt;
  
  
  Ratings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;$that&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Movie"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.userId&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rtg&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rtg&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idFrom&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Rating"&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.movieId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.userId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row.rating&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;u.name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;u:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;rtg.rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row.rating&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rtg.timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toInteger&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row.timestamp&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt;
  &lt;span class="py"&gt;rtg:&lt;/span&gt;&lt;span class="n"&gt;Rating&lt;/span&gt;
&lt;span class="k"&gt;MERGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;u:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:SUBMITTED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;rtg:&lt;/span&gt;&lt;span class="n"&gt;Rating&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_RATING&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MERGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;u:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:RATED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last ingest query processes rows from the &lt;code&gt;ratingData.csv&lt;/code&gt; file. The query creates User and Rating nodes, then relates them together. &lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Recipe
&lt;/h2&gt;

&lt;p&gt;As my project progressed, I developed a &lt;a href="https://docs.quine.io//core-concepts/about-recipes.html#what-is-a-recipe" rel="noopener noreferrer"&gt;Quine recipe&lt;/a&gt; to load my &lt;code&gt;CSV&lt;/code&gt; files and perform the analysis. Running the recipe requires a couple of Quine options to pass in the locations of the &lt;code&gt;CSV&lt;/code&gt; files and an updated configuration setting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-Dquine&lt;/span&gt;.in-memory-soft-node-limit&lt;span class="o"&gt;=&lt;/span&gt;30000 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-jar&lt;/span&gt; ../releases/latest &lt;span class="nt"&gt;-r&lt;/span&gt; movieData &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--recipe-value&lt;/span&gt; &lt;span class="nv"&gt;movie_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;movieData.csv &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--recipe-value&lt;/span&gt; &lt;span class="nv"&gt;rating_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ratingData.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After ingesting the &lt;code&gt;CSV&lt;/code&gt; files, it results in the data set stored in Quine:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0xn25prwet0rnlybht9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0xn25prwet0rnlybht9.png" alt="The data model in Quine for the IMDB data." width="800" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The orange Movie and Person nodes are created directly from the &lt;code&gt;Entity&lt;/code&gt; column in &lt;code&gt;movieData.csv&lt;/code&gt;. The User node is from &lt;code&gt;ratingData.csv&lt;/code&gt; and the green nodes were derived from data stored within an entity row. The &lt;code&gt;ActedDirected&lt;/code&gt; relationship is built by the standing query in the recipe. &lt;/p&gt;

&lt;h2&gt;
  
  
  Answering the Question
&lt;/h2&gt;

&lt;p&gt;Getting all of this data into Quine was only part of the challenge. Remember the question that we were asked, &lt;em&gt;"which actors have acted in and directed the same movie?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Quine is a streaming graph; if we were to connect the ingest streams to the streaming source, rather than &lt;code&gt;CSV&lt;/code&gt; files, the &lt;a href="https://docs.quine.io/components/writing-standing-queries.html" rel="noopener noreferrer"&gt;standing query&lt;/a&gt; inside of the recipe that I developed would answer the question for movies in the past as well as movies in the future.  &lt;/p&gt;

&lt;p&gt;Our standing query matches when a complete pattern for the situation when an actor (&lt;code&gt;Person&lt;/code&gt;) both &lt;code&gt;ACTED_IN&lt;/code&gt; and &lt;code&gt;DIRECTED&lt;/code&gt; the same movie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ACTED_IN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:DIRECTED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;movieId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m.title&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Movie&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;personId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.name&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the standing query completes a match, it processes the movie &lt;code&gt;id&lt;/code&gt; and person &lt;code&gt;id&lt;/code&gt;  through the &lt;a href="https://docs.quine.io/components/standing-query-outputs.html" rel="noopener noreferrer"&gt;output&lt;/a&gt; query and actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;standingQueries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cypher&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MultipleValues&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
        &lt;span class="s"&gt;MATCH (a:Movie)&amp;lt;-[:ACTED_IN]-(p:Person)-[:DIRECTED]-&amp;gt;(m:Movie) &lt;/span&gt;
        &lt;span class="s"&gt;WHERE id(a) = id(m)&lt;/span&gt;
        &lt;span class="s"&gt;RETURN id(m) as movieId, m.title as Movie, id(p) as personId, p.name as Actor&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;set-ActedDirected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CypherQuery&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;MATCH (m),(p)&lt;/span&gt;
          &lt;span class="s"&gt;WHERE strId(m) = $that.data.movie AND strId(p) = $that.data.person&lt;/span&gt;
          &lt;span class="s"&gt;MERGE (p:Person)-[:ActedDirected]-&amp;gt;(m:Movie)&lt;/span&gt;
      &lt;span class="na"&gt;log-actor-director&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WriteToFile&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ActorDirector.jsonl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My standing query creates a new &lt;code&gt;ActedDirected&lt;/code&gt; relationship between the Person and Movie nodes, then logs the relationship. &lt;/p&gt;

&lt;p&gt;Four hundred ninety-one actors acted in and directed the same movie in our data set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Actor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Clint Eastwood"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Movie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unforgiven"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"movieId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4a6d64c8-9c90-3362-b443-4d2e7b2fb9d1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"personId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4638a820-3b68-3fc7-9fa7-341e876b701e"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Phew, we made it through! And we learned a lot along the way. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSV data is streamed into Quine&lt;/li&gt;
&lt;li&gt;Quine can read from external files and streaming providers &lt;/li&gt;
&lt;li&gt;You can ingest multiple streams at once, movies and reviewers, and combine them into one streaming graph&lt;/li&gt;
&lt;li&gt;Always separate ingest queries using the jobs to be done framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quine is open source if you want to run this analysis for yourself. Download a precompiled version or build it yourself from the codebase (&lt;a href="https://github.com/thatdot/quine" rel="noopener noreferrer"&gt;Quine Github&lt;/a&gt;). I published the recipe that I developed at &lt;code&gt;https://quine.io/recipes&lt;/code&gt;. The page has instructions for downloading the &lt;code&gt;CSV&lt;/code&gt; files and running the recipe. &lt;/p&gt;

&lt;p&gt;Have a question, suggestion, or improvement? I welcome your feedback! Please drop in to &lt;a href="https://quine-io.slack.com/" rel="noopener noreferrer"&gt;Quine Slack&lt;/a&gt; and let me know. I'm always happy to discuss Quine or answer questions. &lt;/p&gt;

</description>
      <category>quine</category>
      <category>streaminggraph</category>
      <category>datamodel</category>
    </item>
  </channel>
</rss>
