Add tutorial articles to docs/user_technical
Summary: Quick start summary: - Quick Start -- Installation --- Debian Package Installation --- RPM Package Installation --- Docker Installation ---- Note about Named Volumes ---- Note for OS X/macOS users -- Querying --- Supported Languages -- Telemetry -- Where to Next Example (TED/football/europe) summary: - Article Name -- Introduction -- Data Model -- Importing the Snapshot -- Example Queries Programmatic Querying summary: - Programmatic Querying -- Supported Languages -- Secure Sockets Layer (SSL) -- Examples --- Python Example --- Java Example --- JavaScript Example --- C# Example -- Limitations --- Multiple Users & Authorization Reviewers: buda, dtomicevic, teon.banek, mtomic Reviewed By: buda, teon.banek Differential Revision: https://phabricator.memgraph.io/D1516
This commit is contained in:
parent
4d3c0a2fa5
commit
da89dcb3ee
176
docs/user_technical/tutorial__analyzing_TED_talks.md
Normal file
176
docs/user_technical/tutorial__analyzing_TED_talks.md
Normal file
@ -0,0 +1,176 @@
|
||||
## Analyzing TED Talks
|
||||
|
||||
This article is a part of a series intended to show users how to use Memgraph
|
||||
on real-world data and, by doing so, retrieve some interesting and useful
|
||||
information.
|
||||
|
||||
We highly recommend checking out the other articles from this series:
|
||||
|
||||
* [Exploring the European Road Network](tutorial__exploring_the_european_road_network.md)
|
||||
* [Graphing the Premier League](tutorial__graphing_the_premier_league.md)
|
||||
|
||||
### Introduction
|
||||
|
||||
[TED](https://www.ted.com/) is a nonprofit organization devoted to spreading
|
||||
ideas, usually in the form of short, powerful talks.
|
||||
Today, TED talks are influential videos from expert speakers on almost all
|
||||
topics — from science to business to global issues.
|
||||
Here we present a small dataset which consists of 97 talks, show how to model
|
||||
this data as a graph and demonstrate a few example queries.
|
||||
|
||||
### Data Model
|
||||
|
||||
Each TED talk has a main speaker, so we
|
||||
identify two types of nodes — `Talk` and `Speaker`. Also, we will add
|
||||
an edge of type `Gave` pointing to a `Talk` from its main `Speaker`.
|
||||
Each speaker has a name so we can add property `name` to `Speaker` node.
|
||||
Likewise, we'll add properties `name`, `title` and `description` to node
|
||||
`Talk`. Furthermore, each talk is given in a specific TED event, so we can
|
||||
create node `Event` with property `name` and relationship `InEvent` between
|
||||
talk and event.
|
||||
|
||||
Talks are tagged with keywords to facilitate searching, hence we
|
||||
add node `Tag` with property `name` and relationship `HasTag` between talk and
|
||||
tag. Moreover, users give ratings to each talk by selecting up to three
|
||||
predefined string values. Therefore we add node `Rating` with these values as
|
||||
property `name` and relationship`HasRating` with property `user_count` between
|
||||
talk and rating nodes.
|
||||
|
||||
### Importing the Snapshot
|
||||
|
||||
We have prepared a database snapshot for this example, so the user can easily
|
||||
import it when starting Memgraph using the `--durability-directory` option.
|
||||
|
||||
```bash
|
||||
/usr/lib/memgraph/memgraph --durability-directory /usr/share/memgraph/examples/TEDTalk \
|
||||
--durability-enabled=false --snapshot-on-exit=false
|
||||
```
|
||||
|
||||
When using Memgraph installed from DEB or RPM package, the currently running
|
||||
Memgraph server may need to be stopped before importing the example. The user
|
||||
can do so using the following command:
|
||||
|
||||
```bash
|
||||
systemctl stop memgraph
|
||||
```
|
||||
|
||||
When using Docker, the example can be imported with the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 7687:7687 \
|
||||
-v mg_lib:/var/lib/memgraph -v mg_log:/var/log/memgraph -v mg_etc:/etc/memgraph \
|
||||
memgraph --durability-directory /usr/share/memgraph/examples/TEDTalk \
|
||||
--durability-enabled=false --snapshot-on-exit=false
|
||||
```
|
||||
|
||||
The user should note that any modifications of the database state will persist
|
||||
only during this run of Memgraph.
|
||||
|
||||
### Example Queries
|
||||
|
||||
1) Find all talks given by specific speaker:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Speaker {name: "Hans Rosling"})-[:Gave]->(m:Talk)
|
||||
RETURN m.title;
|
||||
```
|
||||
|
||||
2) Find the top 20 speakers with most talks given:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Speaker)-[:Gave]->(m)
|
||||
RETURN n.name, COUNT(m) AS TalksGiven
|
||||
ORDER BY TalksGiven DESC LIMIT 20;
|
||||
```
|
||||
|
||||
3) Find talks related by tag to specific talk and count them:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Talk {name: "Michael Green: Why we should build wooden skyscrapers"})
|
||||
-[:HasTag]->(t:Tag)<-[:HasTag]-(m:Talk)
|
||||
WITH * ORDER BY m.name
|
||||
RETURN t.name, COLLECT(m.name), COUNT(m) AS TalksCount
|
||||
ORDER BY TalksCount DESC;
|
||||
```
|
||||
|
||||
4) Find 20 most frequently used tags:
|
||||
|
||||
```opencypher
|
||||
MATCH (t:Tag)<-[:HasTag]-(n:Talk)
|
||||
RETURN t.name AS Tag, COUNT(n) AS TalksCount
|
||||
ORDER BY TalksCount DESC, Tag LIMIT 20;
|
||||
```
|
||||
|
||||
5) Find 20 talks most rated as "Funny". If you want to query by other ratings,
|
||||
possible values are: Obnoxious, Jaw-dropping, OK, Persuasive, Beautiful,
|
||||
Confusing, Longwinded, Unconvincing, Fascinating, Ingenious, Courageous, Funny,
|
||||
Informative and Inspiring.
|
||||
|
||||
```opencypher
|
||||
MATCH (r:Rating{name:"Funny"})<-[e:HasRating]-(m:Talk)
|
||||
RETURN m.name, e.user_count ORDER BY e.user_count DESC LIMIT 20;
|
||||
```
|
||||
|
||||
6) Find inspiring talks and their speakers from the field of technology:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Talk)-[:HasTag]->(m:Tag {name: "technology"})
|
||||
MATCH (n)-[r:HasRating]->(p:Rating {name: "Inspiring"})
|
||||
MATCH (n)<-[:Gave]-(s:Speaker)
|
||||
WHERE r.user_count > 1000
|
||||
RETURN n.title, s.name, r.user_count ORDER BY r.user_count DESC;
|
||||
```
|
||||
|
||||
7) Now let's see one real-world example — how to make a real-time
|
||||
recommendation. If you've just watched a talk from a certain
|
||||
speaker (e.g. Hans Rosling) you might be interested in finding more talks from
|
||||
the same speaker on a similar topic:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Speaker {name: "Hans Rosling"})-[:Gave]->(m:Talk)
|
||||
MATCH (t:Talk {title: "New insights on poverty"})-[:HasTag]->(tag:Tag)<-[:HasTag]-(m)
|
||||
WITH * ORDER BY tag.name
|
||||
RETURN m.title as Title, COLLECT(tag.name), COUNT(tag) as TagCount
|
||||
ORDER BY TagCount DESC, Title;
|
||||
```
|
||||
|
||||
The following few queries are focused on extracting information about
|
||||
TED events.
|
||||
|
||||
8) Find how many talks were given per event:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Event)<-[:InEvent]-(t:Talk)
|
||||
RETURN n.name as Event, COUNT(t) AS TalksCount
|
||||
ORDER BY TalksCount DESC, Event
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
9) Find the most popular tags in the specific event:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Event {name:"TED2006"})<-[:InEvent]-(t:Talk)-[:HasTag]->(tag:Tag)
|
||||
RETURN tag.name as Tag, COUNT(t) AS TalksCount
|
||||
ORDER BY TalksCount DESC, Tag
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
10) Discover which speakers participated in more than 2 events:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Speaker)-[:Gave]->(t:Talk)-[:InEvent]->(e:Event)
|
||||
WITH n, COUNT(e) AS EventsCount WHERE EventsCount > 2
|
||||
RETURN n.name as Speaker, EventsCount
|
||||
ORDER BY EventsCount DESC, Speaker;
|
||||
```
|
||||
|
||||
11) For each speaker search for other speakers that participated in same
|
||||
events:
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Speaker)-[:Gave]->()-[:InEvent]->(e:Event)<-[:InEvent]-()<-[:Gave]-(m:Speaker)
|
||||
WHERE n.name != m.name
|
||||
WITH DISTINCT n, m ORDER BY m.name
|
||||
RETURN n.name AS Speaker, COLLECT(m.name) AS Others
|
||||
ORDER BY Speaker;
|
||||
```
|
@ -0,0 +1,178 @@
|
||||
## Exploring the European Road Network
|
||||
|
||||
This article is a part of a series intended to show users how to use Memgraph
|
||||
on real-world data and, by doing so, retrieve some interesting and useful
|
||||
information.
|
||||
|
||||
We highly recommend checking out the other articles from this series:
|
||||
|
||||
* [Analyzing TED Talks](tutorial__analyzing_ted_talks.md)
|
||||
* [Graphing the Premier League](tutorial__graphing_the_premier_league.md)
|
||||
|
||||
### Introduction
|
||||
|
||||
This particular article outlines how to use some of Memgraph's built-in graph
|
||||
algorithms. More specifically, the article shows how to use breadth-first search
|
||||
graph traversal algorithm, and Dijkstra's algorithm for finding weighted
|
||||
shortest paths between nodes in the graph.
|
||||
|
||||
### Data model
|
||||
|
||||
One of the most common applications of graph traversal algorithms is driving
|
||||
route computation, so we will use European road network graph as an example.
|
||||
The graph consists of 999 major European cities from 39 countries in total.
|
||||
Each city is connected to the country it belongs to via an edge of type `:In_`.
|
||||
There are edges of type `:Road` connecting cities less than 500 kilometers
|
||||
apart. Distance between cities is specified in the `length` property of the
|
||||
edge.
|
||||
|
||||
### Importing the Snapshot
|
||||
|
||||
We have prepared a database snapshot for this example, so the user can easily
|
||||
import it when starting Memgraph using the `--durability-directory` option.
|
||||
|
||||
```bash
|
||||
/usr/lib/memgraph/memgraph --durability-directory /usr/share/memgraph/examples/Europe \
|
||||
--durability-enabled=false --snapshot-on-exit=false
|
||||
```
|
||||
|
||||
When using Memgraph installed from DEB or RPM package, the currently running
|
||||
Memgraph server may need to be stopped before importing the example. The user
|
||||
can do so using the following command:
|
||||
|
||||
```bash
|
||||
systemctl stop memgraph
|
||||
```
|
||||
|
||||
When using Docker, the example can be imported with the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 7687:7687 \
|
||||
-v mg_lib:/var/lib/memgraph -v mg_log:/var/log/memgraph -v mg_etc:/etc/memgraph \
|
||||
memgraph --durability-directory /usr/share/memgraph/examples/Europe \
|
||||
--durability-enabled=false --snapshot-on-exit=false
|
||||
```
|
||||
|
||||
The user should note that any modifications of the database state will persist
|
||||
only during this run of Memgraph.
|
||||
|
||||
### Example Queries
|
||||
|
||||
1) Let's list all of the countries in our road network.
|
||||
|
||||
```opencypher
|
||||
MATCH (c:Country) RETURN c.name ORDER BY c.name;
|
||||
```
|
||||
|
||||
2) Which Croatian cities are in our road network?
|
||||
|
||||
```opencypher
|
||||
MATCH (c:City)-[:In_]->(:Country {name: "Croatia"})
|
||||
RETURN c.name ORDER BY c.name;
|
||||
```
|
||||
|
||||
3) Which cities in our road network are less than 200 km away from Zagreb?
|
||||
|
||||
```opencypher
|
||||
MATCH (:City {name: "Zagreb"})-[r:Road]->(c:City)
|
||||
WHERE r.length < 200
|
||||
RETURN c.name ORDER BY c.name;
|
||||
```
|
||||
|
||||
Now let's try some queries using Memgraph's graph traversal capabilities.
|
||||
|
||||
4) Say you want to drive from Zagreb to Paris. You might wonder, what is the
|
||||
least number of cities you have to visit if you don't want to drive more than
|
||||
500 kilometers between stops. Since the edges in our road network don't connect
|
||||
cities that are more than 500 km apart, this is a great use case for the
|
||||
breadth-first search (BFS) algorithm.
|
||||
|
||||
```opencypher
|
||||
MATCH p = (:City {name: "Zagreb"})
|
||||
-[:Road * bfs]->
|
||||
(:City {name: "Paris"})
|
||||
RETURN nodes(p);
|
||||
```
|
||||
|
||||
5) What if we want to bike to Paris instead of driving? It is unreasonable (and
|
||||
dangerous!) to bike 500 km per day. Let's limit ourselves to biking no more
|
||||
than 200 km in one go.
|
||||
|
||||
```opencypher
|
||||
MATCH p = (:City {name: "Zagreb"})
|
||||
-[:Road * bfs (e, v | e.length <= 200)]->
|
||||
(:City {name: "Paris"})
|
||||
RETURN nodes(p);
|
||||
```
|
||||
|
||||
"What is this special syntax?", you might wonder.
|
||||
|
||||
`(e, v | e.length <= 200)` is called a *filter lambda*. It's a function that
|
||||
takes an edge symbol `e` and a vertex symbol `v` and decides whether this edge
|
||||
and vertex pair should be considered valid in breadth-first expansion by
|
||||
returning true or false (or Null). In the above example, lambda is returning
|
||||
true if edge length is not greater than 200, because we don't want to bike more
|
||||
than 200 km in one go.
|
||||
|
||||
6) Let's say we also don't want to visit Vienna on our way to Paris, because we
|
||||
have a lot of friends there and visiting all of them would take up a lot of our
|
||||
time. We just have to update our filter lambda.
|
||||
|
||||
```opencypher
|
||||
MATCH p = (:City {name: "Zagreb"})
|
||||
-[:Road * bfs (e, v | e.length <= 200 AND v.name != "Vienna")]->
|
||||
(:City {name: "Paris"})
|
||||
RETURN nodes(p);
|
||||
```
|
||||
|
||||
As you can see, without the additional restriction we could visit 11 cities. If
|
||||
we want to avoid Vienna, we must visit at least 12 cities.
|
||||
|
||||
7) Instead of counting the cities visited, we might want to find the shortest
|
||||
paths in terms of distance travelled. This is a textbook application of
|
||||
Dijkstra's algorithm. The following query will return the list of cities on the
|
||||
shortest path from Zagreb to Paris along with the total length of the path.
|
||||
|
||||
```opencypher
|
||||
MATCH p = (:City {name: "Zagreb"})
|
||||
-[:Road * wShortest (e, v | e.length) total_weight]->
|
||||
(:City {name: "Paris"})
|
||||
RETURN nodes(p) as cities, total_weight;
|
||||
```
|
||||
|
||||
As you can see, the syntax is quite similar to breadth-first search syntax.
|
||||
Instead of a filter lambda, we need to provide a *weight lambda* and the *total
|
||||
weight symbol*. Given an edge and vertex pair, weight lambda must return the
|
||||
cost of expanding to the given vertex using the given edge. The path returned
|
||||
will have the smallest possible sum of costs and it will be stored in the total
|
||||
weight symbol. A limitation of Dijkstra's algorithm is that the cost must be
|
||||
non-negative.
|
||||
|
||||
8) We can also combine weight and filter lambdas in the shortest-path query.
|
||||
Let's say we're interested in the shortest path that doesn't require travelling
|
||||
more that 200 km in one go for our bike route.
|
||||
|
||||
```opencypher
|
||||
MATCH p = (:City {name: "Zagreb"})
|
||||
-[:Road * wShortest (e, v | e.length) total_weight (e, v | e.length <= 200)]->
|
||||
(:City {name: "Paris"})
|
||||
RETURN nodes(p) as cities, total_weight;
|
||||
```
|
||||
|
||||
9) Let's try and find 10 cities that are furthest away from Zagreb.
|
||||
|
||||
```opencypher
|
||||
MATCH (:City {name: "Zagreb"})
|
||||
-[:Road * wShortest (e, v | e.length) total_weight]->
|
||||
(c:City)
|
||||
RETURN c, total_weight
|
||||
ORDER BY total_weight DESC LIMIT 10;
|
||||
```
|
||||
|
||||
It is not surprising to see that they are all in Siberia.
|
||||
|
||||
To learn more about these algorithms, we suggest you check out their Wikipedia
|
||||
pages:
|
||||
|
||||
* [Breadth-first search](https://en.wikipedia.org/wiki/Breadth-first_search)
|
||||
* [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)
|
190
docs/user_technical/tutorial__graphing_the_premier_league.md
Normal file
190
docs/user_technical/tutorial__graphing_the_premier_league.md
Normal file
@ -0,0 +1,190 @@
|
||||
## Graphing the Premier League
|
||||
|
||||
This article is a part of a series intended to show users how to use Memgraph
|
||||
on real-world data and, by doing so, retrieve some interesting and useful
|
||||
information.
|
||||
|
||||
We highly recommend checking out the other articles from this series:
|
||||
|
||||
* [Analyzing TED Talks](tutorial__analyzing_ted_talks.md)
|
||||
* [Exploring the European Road Network](tutorial__exploring_the_european_road_network.md)
|
||||
|
||||
### Introduction
|
||||
|
||||
[Football](https://en.wikipedia.org/wiki/Association_football)
|
||||
is a team sport played between two teams of eleven
|
||||
players with a spherical ball. The game is played on a rectangular pitch with
|
||||
a goal at each and. The object of the game is to score by moving the ball
|
||||
beyond the goal line into the opposing goal. The game is played by more than
|
||||
250 million players in over 200 countries, making it the world's most
|
||||
popular sport.
|
||||
|
||||
In this article, we will present a graph model of a reasonably sized dataset
|
||||
of football matches across world's most popular leagues.
|
||||
|
||||
### Data Model
|
||||
|
||||
In essence, we are trying to model a set of football matches. All information
|
||||
about a single match is going to be contained in three nodes and two edges.
|
||||
Two of the nodes will represent the teams that have played the match, while the
|
||||
third node will represent the game itself. Both edges are directed from the
|
||||
team nodes to the game node and are labeled as `:Played`.
|
||||
|
||||
Let us consider a real life example of this model—Arsene Wenger's 1000th
|
||||
game in charge of Arsenal. This was a regular fixture of a 2013/2014
|
||||
English Premier League, yet it was written in the stars that this historic
|
||||
moment would be a big London derby against Chelsea on Stanford Bridge. The
|
||||
sketch below shows how this game is being modeled in our database.
|
||||
|
||||
```
|
||||
+---------------+ +-----------------------------+
|
||||
|n: Team | |w: Game |
|
||||
| |-[:Played {side: "home", outcome: "won"}]-->| |
|
||||
|name: "Chelsea"| |HT_home_score: 4 |
|
||||
+---------------+ |HT_away_score: 0 |
|
||||
|HT_result: "H" |
|
||||
|FT_home_score: 6 |
|
||||
|FT_away_score: 0 |
|
||||
|FT_result: "H" |
|
||||
+---------------+ |date: "2014-03-22" |
|
||||
|m: Team | |league: "ENG-Premier League" |
|
||||
| |-[:Played {side: "away", outcome: "lost"}]->|season: 2013 |
|
||||
|name: "Arsenal"| |referee: "Andre Marriner" |
|
||||
+---------------+ +-----------------------------+
|
||||
```
|
||||
|
||||
### Importing the Snapshot
|
||||
|
||||
We have prepared a database snapshot for this example, so the user can easily
|
||||
import it when starting Memgraph using the `--durability-directory` option.
|
||||
|
||||
```bash
|
||||
/usr/lib/memgraph/memgraph --durability-directory /usr/share/memgraph/examples/football \
|
||||
--durability-enabled=false --snapshot-on-exit=false
|
||||
```
|
||||
|
||||
When using Memgraph installed from DEB or RPM package, the currently running
|
||||
Memgraph server may need to be stopped before importing the example. The user
|
||||
can do so using the following command:
|
||||
|
||||
```bash
|
||||
systemctl stop memgraph
|
||||
```
|
||||
|
||||
When using Docker, the example can be imported with the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 7687:7687 \
|
||||
-v mg_lib:/var/lib/memgraph -v mg_log:/var/log/memgraph -v mg_etc:/etc/memgraph \
|
||||
memgraph --durability-directory /usr/share/memgraph/examples/football \
|
||||
--durability-enabled=false --snapshot-on-exit=false
|
||||
```
|
||||
|
||||
The user should note that any modifications of the database state will persist
|
||||
only during this run of Memgraph.
|
||||
|
||||
### Example Queries
|
||||
|
||||
1) You might wonder, what leagues are supported?
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Game)
|
||||
RETURN DISTINCT n.league AS League
|
||||
ORDER BY League;
|
||||
```
|
||||
|
||||
2) We have stored a certain number of seasons for each league. What is the
|
||||
oldest/newest season we have included?
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Game)
|
||||
RETURN DISTINCT n.league AS League, MIN(n.season) AS Oldest, MAX(n.season) AS Newest
|
||||
ORDER BY League;
|
||||
```
|
||||
|
||||
3) You have already seen one game between Chelsea and Arsenal, let's list all of
|
||||
them in chronological order.
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Team {name: "Chelsea"})-[e:Played]->(w:Game)<-[f:Played]-(m:Team {name: "Arsenal"})
|
||||
RETURN w.date AS Date, e.side AS Chelsea, f.side AS Arsenal,
|
||||
w.FT_home_score AS home_score, w.FT_away_score AS away_score
|
||||
ORDER BY Date;
|
||||
```
|
||||
|
||||
4) How about filtering games in which Chelsea won?
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Team {name: "Chelsea"})-[e:Played {outcome: "won"}]->
|
||||
(w:Game)<-[f:Played]-(m:Team {name: "Arsenal"})
|
||||
RETURN w.date AS Date, e.side AS Chelsea, f.side AS Arsenal,
|
||||
w.FT_home_score AS home_score, w.FT_away_score AS away_score
|
||||
ORDER BY Date;
|
||||
```
|
||||
|
||||
5) Home field advantage is a thing in football. Let's list the number of home
|
||||
defeats for each Premier League team in the 2016/2017 season.
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Team)-[:Played {side: "home", outcome: "lost"}]->
|
||||
(w:Game {league: "ENG-Premier League", season: 2016})
|
||||
RETURN n.name AS Team, count(w) AS home_defeats
|
||||
ORDER BY home_defeats, Team;
|
||||
```
|
||||
|
||||
6) At the end of the season the team with the most points wins the league. For
|
||||
each victory, a team is awarded 3 points and for each draw it is awarded
|
||||
1 point. Let's find out how many points did reigning champions (Chelsea) have
|
||||
at the end of 2016/2017 season.
|
||||
|
||||
```opencypher
|
||||
MATCH (n:Team {name: "Chelsea"})-[:Played {outcome: "drew"}]->(w:Game {season: 2016})
|
||||
WITH n, COUNT(w) AS draw_points
|
||||
MATCH (n)-[:Played {outcome: "won"}]->(w:Game {season: 2016})
|
||||
RETURN draw_points + 3 * COUNT(w) AS total_points;
|
||||
```
|
||||
|
||||
7) In fact, why not retrieve the whole table?
|
||||
|
||||
```opencypher
|
||||
MATCH (n)-[:Played {outcome: "drew"}]->(w:Game {league: "ENG-Premier League", season: 2016})
|
||||
WITH n, COUNT(w) AS draw_points
|
||||
MATCH (n)-[:Played {outcome: "won"}]->(w:Game {league: "ENG-Premier League", season: 2016})
|
||||
RETURN n.name AS Team, draw_points + 3 * COUNT(w) AS total_points
|
||||
ORDER BY total_points DESC;
|
||||
```
|
||||
|
||||
8) People have always debated which of the major leagues is the most exciting.
|
||||
One basic metric is the average number of goals per game. Let's see the results
|
||||
at the end of the 2016/2017 season. WARNING: This might shock you.
|
||||
|
||||
```opencypher
|
||||
MATCH (w:Game {season: 2016})
|
||||
RETURN w.league, AVG(w.FT_home_score) + AVG(w.FT_away_score) AS avg_goals_per_game
|
||||
ORDER BY avg_goals_per_game DESC;
|
||||
```
|
||||
|
||||
9) Another metric might be the number of comebacks—games where one side
|
||||
was winning at half time but were overthrown by the other side by the end
|
||||
of the match. Let's count such occurrences during all supported seasons across
|
||||
all supported leagues.
|
||||
|
||||
```opencypher
|
||||
MATCH (g:Game) WHERE
|
||||
(g.HT_result = "H" AND g.FT_result = "A") OR
|
||||
(g.HT_result = "A" AND g.FT_result = "H")
|
||||
RETURN g.league AS League, count(g) AS Comebacks
|
||||
ORDER BY Comebacks DESC;
|
||||
```
|
||||
|
||||
10) Exciting leagues also tend to be very unpredictable. On that note, let's
|
||||
list all triplets of teams where, during the course of one season, team A won
|
||||
against team B, team B won against team C and team C won against team A.
|
||||
|
||||
```opencypher
|
||||
MATCH (a)-[:Played {outcome: "won"}]->(p:Game {league: "ENG-Premier League", season: 2016})<--
|
||||
(b)-[:Played {outcome: "won"}]->(q:Game {league: "ENG-Premier League", season: 2016})<--
|
||||
(c)-[:Played {outcome: "won"}]->(r:Game {league: "ENG-Premier League", season: 2016})<--(a)
|
||||
WHERE p.date < q.date AND q.date < r.date
|
||||
RETURN a.name AS Team1, b.name AS Team2, c.name AS Team3;
|
||||
```
|
224
docs/user_technical/tutorial__programmatic_querying.md
Normal file
224
docs/user_technical/tutorial__programmatic_querying.md
Normal file
@ -0,0 +1,224 @@
|
||||
## Programmatic Querying
|
||||
|
||||
### Supported Languages
|
||||
|
||||
If users wish to query Memgraph programmatically, they can do so using the
|
||||
[Bolt protocol](https://boltprotocol.org). Bolt was designed for efficient
|
||||
communication with graph databases and Memgraph supports
|
||||
[Version 1](https://boltprotocol.org/v1) of the protocol. Bolt protocol drivers
|
||||
for some popular programming languages are listed below:
|
||||
|
||||
* [Java](https://github.com/neo4j/neo4j-java-driver)
|
||||
* [Python](https://github.com/neo4j/neo4j-python-driver)
|
||||
* [JavaScript](https://github.com/neo4j/neo4j-javascript-driver)
|
||||
* [C#](https://github.com/neo4j/neo4j-dotnet-driver)
|
||||
* [Ruby](https://github.com/neo4jrb/neo4j)
|
||||
* [Haskell](https://github.com/zmactep/hasbolt)
|
||||
* [PHP](https://github.com/graphaware/neo4j-bolt-php)
|
||||
|
||||
### Secure Sockets Layer (SSL)
|
||||
|
||||
Secure connections are supported and enabled by default. The server initially
|
||||
ships with a self-signed testing certificate. The certificate can be replaced
|
||||
by editing the following parameters in `/etc/memgraph/memgraph.conf`:
|
||||
```
|
||||
--cert-file=/path/to/ssl/certificate.pem
|
||||
--key-file=/path/to/ssl/privatekey.pem
|
||||
```
|
||||
To disable SSL support and use insecure connections to the database you should
|
||||
set both parameters (`--cert-file` and `--key-file`) to empty values.
|
||||
|
||||
### Examples
|
||||
|
||||
In this article we have included some basic usage examples for the following
|
||||
supported languages:
|
||||
|
||||
* [Python](#python-example)
|
||||
* [Java](#java-example)
|
||||
* [JavaScript](#javascript-example)
|
||||
* [C#](#c-sharp-example)
|
||||
|
||||
Examples for the languages listed above are equivalent.
|
||||
|
||||
#### Python Example
|
||||
|
||||
Neo4j officially supports Python for interacting with an openCypher and Bolt
|
||||
compliant database. For details consult the
|
||||
[official documentation](http://neo4j.com/docs/api/python-driver) and the
|
||||
[GitHub project](https://github.com/neo4j/neo4j-python-driver).
|
||||
|
||||
The code snippet below outlines a basic usage example which connects to the
|
||||
database and executes a couple of elementary queries.
|
||||
|
||||
```python
|
||||
from neo4j.v1 import GraphDatabase, basic_auth
|
||||
|
||||
# Initialize and configure the driver.
|
||||
# * provide the correct URL where Memgraph is reachable;
|
||||
# * use an empty user name and password.
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687",
|
||||
auth=basic_auth("", ""))
|
||||
|
||||
# Start a session in which queries are executed.
|
||||
session = driver.session()
|
||||
|
||||
# Execute openCypher queries.
|
||||
# After each query, call either `consume()` or `data()`
|
||||
session.run('CREATE (alice:Person {name: "Alice", age: 22})').consume()
|
||||
|
||||
# Get all the vertices from the database (potentially multiple rows).
|
||||
vertices = session.run('MATCH (n) RETURN n').data()
|
||||
# Assuming we started with an empty database, we should have Alice
|
||||
# as the only row in the results.
|
||||
only_row = vertices.pop()
|
||||
alice = only_row["n"]
|
||||
|
||||
# Print out what we retrieved.
|
||||
print("Found a vertex with labels '{}', name '{}' and age {}".format(
|
||||
alice['name'], alice.labels, alice['age'])
|
||||
|
||||
# Remove all the data from the database.
|
||||
session.run('MATCH (n) DETACH DELETE n').consume()
|
||||
|
||||
# Close the session and the driver.
|
||||
session.close()
|
||||
driver.close()
|
||||
```
|
||||
|
||||
#### Java Example
|
||||
|
||||
The details about Java driver can be found on
|
||||
[GitHub](https://github.com/neo4j/neo4j-java-driver).
|
||||
|
||||
The code snippet below outlines a basic usage example which connects to the
|
||||
database and executes a couple of elementary queries.
|
||||
|
||||
```java
|
||||
import org.neo4j.driver.v1.*;
|
||||
import org.neo4j.driver.v1.types.*;
|
||||
import static org.neo4j.driver.v1.Values.parameters;
|
||||
import java.util.*;
|
||||
|
||||
public class JavaQuickStart {
|
||||
public static void main(String[] args) {
|
||||
// Initialize driver.
|
||||
Config config = Config.build().toConfig();
|
||||
Driver driver = GraphDatabase.driver("bolt://localhost:7687",
|
||||
AuthTokens.basic("",""),
|
||||
config);
|
||||
// Execute basic queries.
|
||||
try (Session session = driver.session()) {
|
||||
StatementResult rs1 = session.run("MATCH (n) DETACH DELETE n");
|
||||
StatementResult rs2 = session.run(
|
||||
"CREATE (alice: Person {name: 'Alice', age: 22})");
|
||||
StatementResult rs3 = session.run( "MATCH (n) RETURN n");
|
||||
List<Record> records = rs3.list();
|
||||
Record record = records.get(0);
|
||||
Node node = record.get("n").asNode();
|
||||
System.out.println(node.get("name").asString());
|
||||
} catch (Exception e) {
|
||||
System.out.println(e);
|
||||
System.exit(1);
|
||||
}
|
||||
// Cleanup.
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### JavaScript Example
|
||||
|
||||
The details about Javascript driver can be found on
|
||||
[GitHub](https://github.com/neo4j/neo4j-javascript-driver).
|
||||
|
||||
Here is an example related to `Node.js`. Memgraph doesn't have integrated
|
||||
support for `WebSocket` which is required during the execution in any web
|
||||
browser. If you want to run `openCypher` queries from a web browser,
|
||||
[websockify](https://github.com/novnc/websockify) has to be up and running.
|
||||
Requests from web browsers are wrapped into `WebSocket` messages, and a proxy
|
||||
is needed to handle the overhead. The proxy has to be configured to point out
|
||||
to Memgraph's Bolt port and web browser driver has to send requests to the
|
||||
proxy port.
|
||||
|
||||
The code snippet below outlines a basic usage example which connects to the
|
||||
database and executes a couple of elementary queries.
|
||||
|
||||
```javascript
|
||||
var neo4j = require('neo4j-driver').v1;
|
||||
var driver = neo4j.driver("bolt://localhost:7687",
|
||||
neo4j.auth.basic("neo4j", "1234"));
|
||||
var session = driver.session();
|
||||
|
||||
function die() {
|
||||
session.close();
|
||||
driver.close();
|
||||
}
|
||||
|
||||
function run_query(query, callback) {
|
||||
var run = session.run(query, {});
|
||||
run.then(callback).catch(function (error) {
|
||||
console.log(error);
|
||||
die();
|
||||
});
|
||||
}
|
||||
|
||||
run_query("MATCH (n) DETACH DELETE n", function (result) {
|
||||
console.log("Database cleared.");
|
||||
run_query("CREATE (alice: Person {name: 'Alice', age: 22})", function (result) {
|
||||
console.log("Record created.");
|
||||
run_query("MATCH (n) RETURN n", function (result) {
|
||||
console.log("Record matched.");
|
||||
var alice = result.records[0].get("n");
|
||||
console.log(alice.labels[0]);
|
||||
console.log(alice.properties["name"]);
|
||||
session.close();
|
||||
driver.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### C# Example {#c-sharp-example}
|
||||
|
||||
The details about C# driver can be found on
|
||||
[GitHub](https://github.com/neo4j/neo4j-dotnet-driver).
|
||||
|
||||
The code snipped below outlines a basic usage example which connects to the
|
||||
database and executes a couple of elementary queries.
|
||||
|
||||
```csh
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Neo4j.Driver.V1;
|
||||
|
||||
public class Basic {
|
||||
public static void Main(string[] args) {
|
||||
// Initialize the driver.
|
||||
var config = Config.DefaultConfig;
|
||||
using(var driver = GraphDatabase.Driver("bolt://localhost:7687", AuthTokens.None, config))
|
||||
using(var session = driver.Session())
|
||||
{
|
||||
// Run basic queries.
|
||||
session.Run("MATCH (n) DETACH DELETE n").Consume();
|
||||
session.Run("CREATE (alice:Person {name: \"Alice\", age: 22})").Consume();
|
||||
var result = session.Run("MATCH (n) RETURN n").First();
|
||||
var alice = (INode) result["n"];
|
||||
Console.WriteLine(alice["name"]);
|
||||
Console.WriteLine(string.Join(", ", alice.Labels));
|
||||
Console.WriteLine(alice["age"]);
|
||||
}
|
||||
Console.WriteLine("All ok!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
Memgraph is currently in an early stage, and therefore has certain limitations
|
||||
we plan to remove in future versions.
|
||||
|
||||
#### Multiple Users & Authorization
|
||||
|
||||
At the moment, Memgraph is single-user only and there is no way to control user
|
||||
privileges, meaning that the default user has read and write privileges over
|
||||
the whole database.
|
291
docs/user_technical/tutorial__quick_start.md
Normal file
291
docs/user_technical/tutorial__quick_start.md
Normal file
@ -0,0 +1,291 @@
|
||||
## Quick Start {#tutorial-quick-start}
|
||||
|
||||
This article briefly outlines the basic steps necessary to install and run
|
||||
Memgraph. It also gives a brief glimpse into the world of OpenCypher and
|
||||
outlines some information on programmatic querying of Memgraph. The users
|
||||
should also make sure to read and fully understand the implications of
|
||||
[telemetry](#telemetry) at the very end of the article.
|
||||
|
||||
### Installation
|
||||
|
||||
With regards to their own preference, users can download the Memgraph binary
|
||||
as:
|
||||
|
||||
* [a Debian package for Debian 9 (Stretch)](#debian-installation)
|
||||
* [a RPM package for CentOS 7](#RPM-installation)
|
||||
* [a Docker image](#docker-installation)
|
||||
|
||||
After downloading the binary, users are advised to proceed to the corresponding
|
||||
section below which outlines the installation details.
|
||||
|
||||
It is important to note that newer versions of Memgraph are currently not
|
||||
backward compatible with older versions. This is mainly noticeable by
|
||||
being unable to load storage snapshots between different versions.
|
||||
|
||||
#### Debian Package Installation {#debian-installation}
|
||||
|
||||
After downloading Memgraph as a Debian package, install it by running the
|
||||
following:
|
||||
|
||||
```bash
|
||||
dpkg -i /path/to/memgraph_<version>.deb
|
||||
```
|
||||
|
||||
On successful installation, Memgraph should already be running. To
|
||||
make sure that is true, user can start it explicitly with the command:
|
||||
|
||||
|
||||
```bash
|
||||
systemctl start memgraph
|
||||
```
|
||||
|
||||
To verify that Memgraph is running, user can run the following command:
|
||||
|
||||
```bash
|
||||
journalctl --unit memgraph
|
||||
```
|
||||
|
||||
If successful, the user should receive an output similar to the following:
|
||||
|
||||
```bash
|
||||
Nov 23 13:40:13 hostname memgraph[14654]: Starting 8 BoltS workers
|
||||
Nov 23 13:40:13 hostname memgraph[14654]: BoltS server is fully armed and operational
|
||||
Nov 23 13:40:13 hostname memgraph[14654]: BoltS listening on 0.0.0.0 at 7687
|
||||
```
|
||||
|
||||
At this point, Memgraph is ready to process queries. To try out some elementary
|
||||
queries, the user should proceed to [querying](#querying) section of this
|
||||
article.
|
||||
|
||||
To shut down the Memgraph server, issue the following command:
|
||||
|
||||
```bash
|
||||
systemctl stop memgraph
|
||||
```
|
||||
|
||||
Memgraph configuration is available in `/etc/memgraph/memgraph.conf`. If the
|
||||
configuration is altered, Memgraph needs to be restarted.
|
||||
|
||||
#### RPM Package Installation {#RPM-installation}
|
||||
|
||||
After downloading the RPM package of Memgraph, the user can install it by
|
||||
issuing the following command:
|
||||
|
||||
```bash
|
||||
rpm -U /path/to/memgraph-<version>.rpm
|
||||
```
|
||||
|
||||
After the successful installation, Memgraph can be started as a service. To do
|
||||
so, the user can type the following command:
|
||||
|
||||
```bash
|
||||
systemctl start memgraph
|
||||
```
|
||||
|
||||
To verify that Memgraph is running, the user should run the following command:
|
||||
|
||||
```bash
|
||||
journalctl --unit memgraph
|
||||
```
|
||||
|
||||
If successful, the user should receive an output similar to the following:
|
||||
|
||||
```bash
|
||||
Nov 23 13:40:13 hostname memgraph[14654]: Starting 8 BoltS workers
|
||||
Nov 23 13:40:13 hostname memgraph[14654]: BoltS server is fully armed and operational
|
||||
Nov 23 13:40:13 hostname memgraph[14654]: BoltS listening on 0.0.0.0 at 7687
|
||||
```
|
||||
|
||||
At this point, Memgraph is ready to process queries. To try out some elementary
|
||||
queries, the user should proceed to [querying](#querying) section of this
|
||||
article.
|
||||
|
||||
To shut down the Memgraph server, issue the following command:
|
||||
|
||||
```bash
|
||||
systemctl stop memgraph
|
||||
```
|
||||
|
||||
Memgraph configuration is available in `/etc/memgraph/memgraph.conf`. If the
|
||||
configuration is altered, Memgraph needs to be restarted.
|
||||
|
||||
#### Docker Installation {#docker-installation}
|
||||
|
||||
Before proceeding with the installation, the user should install the Docker
|
||||
engine on their system. Instructions on how to install Docker can be found on
|
||||
the [official Docker website](https://docs.docker.com/engine/installation).
|
||||
Memgraph's Docker image was built with Docker version `1.12` and should be
|
||||
compatible with all newer versions.
|
||||
|
||||
After successful Docker installation, the user should install the Memgraph
|
||||
Docker image and import it using the following command:
|
||||
|
||||
```bash
|
||||
docker load -i /path/to/memgraph-<version>-docker.tar.gz
|
||||
```
|
||||
|
||||
To actually start Memgraph, the user should issue the following command:
|
||||
|
||||
```bash
|
||||
docker run -p 7687:7687 \
|
||||
-v mg_lib:/var/lib/memgraph -v mg_log:/var/log/memgraph -v mg_etc:/etc/memgraph \
|
||||
memgraph
|
||||
```
|
||||
|
||||
If successful, the user should be greeted with the following message:
|
||||
|
||||
```bash
|
||||
Starting 8 workers
|
||||
Server is fully armed and operational
|
||||
Listening on 0.0.0.0 at 7687
|
||||
```
|
||||
|
||||
At this point, Memgraph is ready to process queries. To try out some elementary
|
||||
queries, the user should proceed to [querying](#querying) section of this
|
||||
article.
|
||||
|
||||
To stop Memgraph, press `Ctrl-c`.
|
||||
|
||||
#### Note about named volumes
|
||||
|
||||
Memgraph configuration is available in Docker's named volume `mg_etc`. On
|
||||
Linux systems it should be in
|
||||
`/var/lib/docker/volumes/mg_etc/_data/memgraph.conf`. After changing the
|
||||
configuration, Memgraph needs to be restarted.
|
||||
|
||||
If it happens that the named volumes are reused between different Memgraph
|
||||
versions, Docker will overwrite a folder within the container with existing
|
||||
data from the host machine. If a new file is introduced, or two versions of
|
||||
Memgraph are not compatible, some features might not work or Memgraph might
|
||||
not be able to work correctly. We strongly advise the users to use another
|
||||
named volume for a different Memgraph version or to remove the existing volume
|
||||
from the host with the following command:
|
||||
|
||||
```bash
|
||||
docker volume rm <volume_name>
|
||||
```
|
||||
#### Note for OS X/macOS Users {#OSX-note}
|
||||
|
||||
Although unlikely, some OS X/macOS users might experience minor difficulties
|
||||
after following the Docker installation instructions. Instead of running on
|
||||
`localhost`, a Docker container for Memgraph might be running on a custom IP
|
||||
address. Fortunately, that IP address can be found using the following
|
||||
algorithm:
|
||||
|
||||
1) Find out the container ID of the Memgraph container
|
||||
|
||||
By issuing the command `docker ps` the user should get an output similar to the
|
||||
following:
|
||||
|
||||
```bash
|
||||
CONTAINER ID IMAGE COMMAND CREATED ...
|
||||
9397623cd87e memgraph "/usr/lib/memgraph/m…" 2 seconds ago ...
|
||||
```
|
||||
|
||||
At this point, it is important to remember the container ID of the Memgraph
|
||||
image. In our case, that is `9397623cd87e`.
|
||||
|
||||
2) Use the container ID to retrieve an IP of the container
|
||||
|
||||
```bash
|
||||
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 9397623cd87e
|
||||
```
|
||||
|
||||
The command above should yield the sought IP. If that IP does not correspond to
|
||||
`localhost`, it should be used instead of `localhost` when firing up the
|
||||
`neo4j-client` in the [querying](#querying) section.
|
||||
|
||||
### Querying {#querying}
|
||||
|
||||
Memgraph supports the openCypher query language which has been developed by
|
||||
[Neo4j](http://neo4j.com). It is a declarative language developed specifically
|
||||
for interaction with graph databases which is currently going through a
|
||||
vendor-independent standardization process.
|
||||
|
||||
The easiest way to execute openCypher queries against Memgraph is by using
|
||||
Neo4j's command-line tool. The command-line `neo4j-client` can be installed as
|
||||
described [on the official website](https://neo4j-client.net).
|
||||
|
||||
After installing `neo4j-client`, the user can connect to the running Memgraph
|
||||
instance by issuing the following shell command:
|
||||
|
||||
```bash
|
||||
neo4j-client -u "" -p "" localhost 7687
|
||||
```
|
||||
|
||||
After the client has started it should present a command prompt similar to:
|
||||
|
||||
```bash
|
||||
neo4j-client 2.1.3
|
||||
Enter `:help` for usage hints.
|
||||
Connected to 'neo4j://@localhost:7687'
|
||||
neo4j>
|
||||
```
|
||||
|
||||
At this point it is possible to execute openCypher queries on Memgraph. Each
|
||||
query needs to end with the `;` (*semicolon*) character. For example:
|
||||
|
||||
```opencypher
|
||||
CREATE (u:User {name: "Alice"})-[:Likes]->(m:Software {name: "Memgraph"});
|
||||
```
|
||||
|
||||
The above will create 2 nodes in the database, one labeled "User" with name
|
||||
"Alice" and the other labeled "Software" with name "Memgraph". It will also
|
||||
create a relationship that "Alice" *likes* "Memgraph".
|
||||
|
||||
To find created nodes and relationships, execute the following query:
|
||||
|
||||
```opencypher
|
||||
MATCH (u:User)-[r]->(x) RETURN u, r, x;
|
||||
```
|
||||
|
||||
#### Supported Languages
|
||||
|
||||
If users wish to query Memgraph programmatically, they can do so using the
|
||||
[Bolt protocol](https://boltprotocol.org). Bolt was designed for efficient
|
||||
communication with graph databases and Memgraph supports
|
||||
[Version 1](https://boltprotocol.org/v1) of the protocol. Bolt protocol drivers
|
||||
for some popular programming languages are listed below:
|
||||
|
||||
* [Java](https://github.com/neo4j/neo4j-java-driver)
|
||||
* [Python](https://github.com/neo4j/neo4j-python-driver)
|
||||
* [JavaScript](https://github.com/neo4j/neo4j-javascript-driver)
|
||||
* [C#](https://github.com/neo4j/neo4j-dotnet-driver)
|
||||
* [Ruby](https://github.com/neo4jrb/neo4j)
|
||||
* [Haskell](https://github.com/zmactep/hasbolt)
|
||||
* [PHP](https://github.com/graphaware/neo4j-bolt-php)
|
||||
|
||||
We have included some basic usage examples for some of the supported languages
|
||||
in the article about [programmatic querying](tutorial__programmatic_querying.md).
|
||||
|
||||
### Telemetry {#telemetry}
|
||||
|
||||
Telemetry is an automated process by which some useful data is collected at
|
||||
a remote point. At Memgraph, we use telemetry for the sole purpose of improving
|
||||
our product, thereby collecting some data about the machine that executes the
|
||||
database (CPU, memory, OS and kernel information) as well as some data about the
|
||||
database runtime (CPU usage, memory usage, vertices and edges count).
|
||||
|
||||
Here at Memgraph, we deeply care about the privacy of our users and do not
|
||||
collect any sensitive information. If users wish to disable Memgraph's telemetry
|
||||
features, they can easily do so by either altering the line in
|
||||
`/etc/memgraph/memgraph.conf` that enables telemetry (`--telemetry-enabled=true`)
|
||||
into `--telemetry-enabled=false`, or by including the `--telemetry-enabled=false`
|
||||
as a command-line argument when running the executable.
|
||||
|
||||
### Where to Next
|
||||
|
||||
To learn more about the openCypher language, the user should visit our
|
||||
[openCypher Query Language](open-cypher.md) article. For real-world examples
|
||||
of how to use Memgraph, we strongly suggest reading through the following
|
||||
articles:
|
||||
|
||||
* [Analyzing TED Talks](tutorial__analyzing_TED_talks.md)
|
||||
* [Graphing the Premier League](tutorial__graphing_the_premier_league.md)
|
||||
* [Exploring the European Road Network](tutorial__exploring_the_european_road_network.md)
|
||||
|
||||
<!--- TODO(ipaljak) Possible broken link on docs update -->
|
||||
Details on what can be stored in Memgraph can be found in the article about
|
||||
[Data Storage](storage.md).
|
||||
|
||||
We *welcome and encourage* your feedback!
|
Loading…
Reference in New Issue
Block a user