Graph-Datenbanken erlauben Abfragen, die sich mit relationalen Systemen nur sehr langsam und teils gar nicht bearbeiten ließen. Mike Schilli zeigt, wie sich damit betrügerische Reviews auf Amazon aufdecken lassen.
Bei näherer Inspektion eines Produkts auf Amazon, das durchgehend mit 5-Sterne-Wertungen glänzt, stellt sich oft heraus, dass viele Bewertungen von professionellen Schergen stammen. Die Texte verraten meist, dass der Schreiberling sich offensichtlich gar nicht mit dem Artikel befasst hat (“Tolles Produkt, schnelle Lieferung!”). Forscht man dann nach weiteren Bewertungen desselben Kunden, finden sich oft weiter 5-Sterne-Reviews mit ähnlichen Satzhülsen. Mittlerweile ist das Problem auf Amazon so offensichtlich, dass sich Kunden verwundert die Augen reiben, warum der Online-Riese nicht endlich einschreitet.
Bei der Ermittlung solcher und ähnlicher Schwindeleien können Graph-Datenbanken helfen. Meist sind es mehrere Kriterien, die Muster im typischen Verhalten von Betrügern erkennen und diese auffliegen lassen. Schreibt ein einziger Kunde Hunderte von 5-Sterne-Bewertungen? Verdächtig. Stehen bei einem Produkt viele dieser 08/15-Bewertungstexte? Daran könnte etwas faul sein. Oder bewerten die Mitglieder eines Gaunerrings alle die gleichen Produkte?
Schlägt ein Alarmsystem bei nur einem dieser Kriterien an, liegt vielleicht nicht unbedingt Missbrauch vor, bei zweien oder mehreren hingegen erhöht sich die Wahrscheinlichkeit eines Schwindels. Weiteres Nachbohren würde sich dann lohnen – sofern ein Interesse daran besteht, Kunden nicht übers Ohr zu hauen.
Erkennungsalgorithmus
Das letzte der vorher genannten Kriterien erscheint programmiertechnisch interessant: Wie findet ein Algorithmus Gruppen von Usern, die alle dieselben Produkte bewerten, ohne dass er Hinweise dafür hat, welche User das nun sind?
Listing 1 zeigt eine fiktive YAML-Liste von Produkten mit den Namen der Bewerter. Eine ähnliche Liste ließe sich mit echten Daten von der Amazon-Webseite mittels der offiziellen API oder einem Scraper einholen.
Listing 1
reviews.yaml
reviews:
product1:
- reviewer1
- reviewer2
- reviewer3
- reviewer7
product2:
- reviewer1
- reviewer2
- reviewer4
- reviewer8
product3:
- reviewer3
product4:
- reviewer4
- reviewer7
product5:
- reviewer5
- reviewer8
product6:
- reviewer6
Das menschliche Auge erkennt sofort, dass das Gaunerduo »reviewer1« und »reviewer2« offensichtlich zusammen die Produkte »product1« und »product2«bewertet hat. Lägen die Daten nun in einem relationalen Datenmodell vor, wäre es sehr aufwendig, diesen Zusammenhang bei einer einigermaßen voluminösen Kundendatenbank in endlicher Zeit herauszufinden.
Mit Graph-Datenbanken, die einfach Relationen zwischen Knoten abwandern, statt mit relationalen Tabellen und teuren Join-Kommandos zu jonglieren, lassen sich aber relativ einfach schlaue Algorithmen programmieren. Der Programmier-Snapshot, seiner Zeit wie immer voraus, hat sich schon vor sechs Jahren mit dem Thema befasst [1]. Die Entwicklung des Genres ist seither aber nicht stehengeblieben, was eine neue Runde legitimiert.
Aufgehübscht
Das in dieser Ausgabe vorgestellte Go-Programm wandelt die YAML-Liste aus Listing 1 in einen Graphen um, der anzeigt, welche Produkte von welchen Personen bewertet wurden.
Hierzu setzt es Kommandos an eine lokal installierte Neo4j-Datenbank ab, die nach Ablauf des Programms den in Abbildung 1 gezeigten Graphen mit den Relationen zwischen Produkten und Rezensenten anzeigt. Der Screenshot stammt aus dem Fenster eines Webbrowsers, der mit »http://localhost:7474« auf eine Neo4j-Installation zeigt, die in einem Container praktischerweise nicht nur den Server bereitstellt, sondern auch ein Web-Interface zur grafischen Aufhübschung der Daten.
Verdächtiges aufspüren
Wurden die Daten erst mal in den Neo4j-Server eingetütet, kann der User mittels interaktiver Kommandos in der sogenannten Cypher-Shell Abfragen absetzen und Analysen starten. Abbildung 2 zeigt den Aufruf des Similarity-Algorithmus [2] aus einem Neo4j-Plugin mit wissenschaftlichen Tools.
Der Algorithmus findet Knoten im Graphen, die über ihre Relationen mit möglichst vielen gemeinsamen Nachbarn verbunden sind, und wertet diese dann als ähnlich. Den numerischen Grad der Ähnlichkeit errechnet er aus dem Jaccard-Koeffizienten [3] der Kandidaten.
Abbildung 2 zeigt das Ergebnis: Offensichtlich hat der Algorithmus festgestellt, dass die beiden Bewerter Reviewer 1 und 2 gemeinsam die Produkte 1 und 2 bewertet haben, und weist den beiden Schlingeln deswegen den numerischen Ähnlichkeitswert 1 zu. Als Beweis für unlautere Machenschaften taugt das freilich noch nicht, aber das Ergebnis zeigt zumindest an, wo man im Verdachtsfall nachbohren könnte, um weitere Indizien aufzudecken.
Interessant am Ergebnis ist auch, dass andere Rezensenten ebenfalls mehrere Produkte bewertet haben, allerdings nicht mit einem Kompagnon dieselben, und deswegen einen niedrigeren Ähnlichkeitswert bekamen. Reviewer 8 hat zum Beispiel die Produkte 2 und 5 bewertet, Reviewer 4 die Produkte 2 und 4. Beide erhielten nur 0,5 auf der Ähnlichkeitsskala, weil ihr Verhalten weniger verdächtig war.
In medias res
Um eine Neo4j-Instanz auf dem heimischen Rechner zu installieren, eignet sich am besten ein Docker-Container, den das Kommando »docker run« aus dem Netz holt und darin einen Neo4j-Server startet (Abbildung 3). Anschließend springt der User mit »docker exec« in den Container und kann dort die interaktive Neo4j-Cypher-Shell öffnen, um Kommandos an den Server abzusetzen.

Abbildung 3: Docker-Kommandos holen Neo4j vom Netz, starten den Server in einem Container und öffnen die interaktive Cypher-Shell.
Damit Browser und API-Skripts von außen auf den containerisierten Neo4j-Server zugreifen können, exportiert der Aufruf in Abbildung 3 die Ports 7474 und 7687 vom Container auf den Host-Rechner. Dort kann dann der User über »http://localhost:7474« mit dem Browser auf den Neo4j-Webserver zugreifen.
Nach dem Einfüttern der Daten in Neo4j zeigt die Browser-Ansicht aus Abbildung 1 auf »http://localhost:7474« das so weit gediehene Relationsmodell. Auf Port 7687 lauscht der Server im Container auf Kommandos der von Neo4j offiziell genutzten Bolt-API, mit der Skripts die Datenbank abfragen und neue Daten einspeisen können.
Weiter verbindet der Docker-Aufruf die Verzeichnisse »data/«, »logs/«, »import/« und »plugins/« des Hosts mit der Innenseite des Containers. So können Host und Container Datenbankdateien und Logs austauschen, und der User darf in »plugins/« neue Plugins aus dem Netz laden und dem Container unterjubeln.
Automatisch einspeisen
Läuft der Server erst einmal im Container, kann das Go-Programm aus der YAML-Liste der Review-Daten eine Reihe von Neo4j-Kommandos formen, um die Relationsdaten in die Datenbank einzuspeisen.
Hierzu legt es zunächst die Knoten vom Typ »Reviewer« und »Product« an, um darauf zwischen beiden eine Relation »reviewed« einzuhängen. Listing 2 zeigt die dafür notwendigen Neo4j-Kommandos, die der User auch von Hand in die Cypher-Shell eingeben könnte.
Listing 2
neo4j-commands.txt
MERGE (product1:Product {name:'product1'})
MERGE (reviewer1:Reviewer {name:'reviewer1'})
MERGE (reviewer1)-[:Reviewed {name: 'reviewed'}]-(product1)
MERGE (reviewer2:Reviewer {name:'reviewer2'})
MERGE (reviewer2)-[:Reviewed {name: 'reviewed'}]-(product1)
MERGE (reviewer3:Reviewer {name:'reviewer3'})
MERGE (reviewer3)-[:Reviewed {name: 'reviewed'}]-(product1)
MERGE (reviewer7:Reviewer {name:'reviewer7'})
MERGE (reviewer7)-[:Reviewed {name: 'reviewed'}]-(product1)
[...]
Dabei erzeugt das »MERGE«-Kommando in Listing 2 jeweils einen neuen Eintrag, entweder einen Knoten oder eine Relation. Das könnte genauso gut ein »CREATE«-Kommando bewerkstelligen, doch »MERGE« flippt nicht gleich aus, falls der Eintrag schon existiert. Zeile 1 legt einen neuen Knoten vom Typ »Product« an, weist ihm das »name«-Attribut »product1« zu und hält eine Referenz darauf in der Variablen »product1« fest. Ähnliches geschieht mit einem »Reviewer«-Knoten in Zeile 2, und Zeile 3 verknüpft dann die vorher definierten Variablen »reviewer1« und »product1« mit einer Beziehung vom Typ »Reviewed«, die das »name«-Attribut auf »reviewed« setzt.
Alle Daten händisch einzugeben, würde den User schnell ermüden. Deshalb automatisiert das Go-Programm aus Listing 3 das Generieren einer Reihe von Neo4j-Kommandos aus der YAML-Liste und setzt sie über Port 7474 an den im Container laufenden Neo4j-Server ab.
Listing 3
rimport.go
package main
import (
"database/sql"
"fmt"
_ "gopkg.in/cq.v1"
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
)
type Config struct {
Reviews map[string][]string
}
func main() {
yamlFile := "reviews.yaml"
data, err := ioutil.ReadFile(yamlFile)
if err != nil {
log.Fatal(err)
}
var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Fatal(err)
}
created := map[string]bool{}
cmd := ""
// nuke all content
toNeo4j(`MATCH (n) OPTIONAL MATCH
(n)-[r]-() DELETE n,r;`)
for prod, reviewers :=
range config.Reviews {
for _, rev := range reviewers {
if _, ok := created[prod]; !ok {
cmd += fmt.Sprintf(
"MERGE (%s:Product {name:'%s'})\n",
prod, prod)
created[prod] = true
}
if _, ok := created[rev]; !ok {
cmd += fmt.Sprintf(
"MERGE (%s:Reviewer {name:'%s'})\n",
rev, rev)
created[rev] = true
}
cmd += fmt.Sprintf(
"MERGE (%s)-[:Reviewed " +
"{name: 'reviewed'}]-(%s)\n",
rev, prod)
}
}
cmd += ";"
toNeo4j(cmd)
}
func toNeo4j(cmd string) {
db, err := sql.Open("neo4j-cypher",
"http://neo4j:test@localhost:7474")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(cmd)
if err != nil {
log.Fatal(err)
}
}
YAML der Go-Welt
Listing 3 nutzt das offizielle YAML-Modul der Go-Welt, um die nach dem Einlesen mit »io/ioutil« als Byte-Array vorliegenden YAML-Daten per »Unmarshal()« in eine Go-Datenstruktur zu transponieren.
Das typstrenge Go integriert das eher legere YAML hier recht salopp, indem es eine über Strings indizierte Hash-Tabelle mit Einträgen definiert, die aus Arrays von Strings bestehen. Die Struktur vom Typ »Config« ab Zeile 12 definiert die Hash-Map mit den verschachtelten String-Arrays unter dem Eintrag »Reviews«. Großschreibung ist hier wichtig, damit das YAML-Modul darauf zugreifen kann.
Ab Zeile 35 iterieren dann zwei For-Schleifen über alle Produkte in der Hash-Map und dann für jeden Eintrag über das Array von Reviewern. Bevor nun Zeile 50 das Kommando für die Relation zusammenstellt, prüfen die If-Bedingungen der Zeilen 38 und 44, ob die beiden Endpunkte der Relation schon als Knoten in der Datenbank existieren.
Zeigt die Map-Variable »created« an, dass ein Knoten noch fehlt, fügt der Code ein »MERGE«-Kommando zur dessen Erzeugung an den »cmd«-String an, der alle Kommandos durch Zeilenumbrüche getrennt kumuliert. In diesem Fall ist es wichtig, Neo4j keine durch Semikolons separierten Kommandos zu schicken. Das führt zu Problemen, falls einige davon Variablen definiert haben (zum Beispiel »reviewer1«), die dann später (beim Erzeugen der Relation) wiederverwendet werden: Ein Semikolon schließt ein Kommando ab, und Neo4j vergisst dann alle vorher definierten Variablen.
Kontakt zum Server
Um dem Server den in »cmd« zusammengebauten Kommando-String zum Einfügen der Daten sowie ein vorausgehendes Kommando zum Löschen aller bisherigen Daten zu schicken, kontaktiert die Funktion »toNeo4j()« ab Zeile 60 den Browser-Port des Servers im Container.
Das verwendete Open-Source-Paket »cq« auf Github ist schon etwas in die Jahre gekommen. Es nutzt zwar nicht den Bolt-Anschluss des offiziell von Neo4j unterstützten API-Moduls auf Port 7687, funktioniert aber dennoch einwandfrei. Zudem lässt es sich einfacher installieren als das Original, das zum Herunterladen irgendwelcher obskurer Bolt-Binaries zwingt.
In SQL-Manier nimmt Zeile 61 Kontakt zum im Container eingedosten Server auf. Zeile 68 schickt mit »Exec()« das in »cmd« vorliegende Kommando über den Port, was der Server mit einer Fehlermeldung quittiert, falls etwas schiefgegangen ist.
Mit der Befehlsfolge aus Listing 4 holt Go die zum Erstellen des Binaries erforderlichen Libraries von Github ab und erzeugt das ausführbare Programm »rimport«. Aufgerufen liest Letzteres erst die Datei »reviews.yaml« von der Platte und pumpt dann die notwendigen Kommandos über den Container-Port an den Neo4j-Server. Anschließend kann der User Abfragen zur Betrugsaufdeckung auf das Datenmodell abschicken, wie vorher in Abbildung 2 gezeigt.
Listing 4
Programm erzeugen
$ go mod init rimport $ go build
Installationswehen
Das aktuelle Docker-Image neo4j:latest schleppt die neueste Neo4j-Version 4.0.3 an, die allerdings noch keinerlei Graph-Algorithmen beherrscht. Um diese nachzuinstallieren, muss der User eine ».jar«-Datei von der Neo4j-Seite herunterladen [4] und im Verzeichnis »~/neo4j/plugins/« ablegen. Dort schnappt sie sich der Docker-Container beim Start des Neo4j-Servers, denn das Kommando »docker run« in Abbildung 3 importiert das Plugin-Verzeichnis über die Option »-v«.
Doch halt, nicht so schnell: Das Graph-Algorithms-Plugin liegt nur in Version 3.5.9 vor. Wer meint, er könne es mir nicht, dir nichts einer Neo4j-Datenbank in Version 4.0.3 unterjubeln, irrt gewaltig. Er sieht den Container gleich nach dem Neustart flugs die Brocken mit einem langen, aber völlig nichtssagenden Stacktrace hinwerfen. Wer allerdings in weiser Voraussicht statt neo4j:latest einfach neo4j:3.5.9 installiert, hat mehr Glück. Der Server startet ordnungsgemäß, und die Datenbankabfrage nach Algorithmen im »algo.*«-Namespace fördert eine lange Liste zutage (Abbildung 4).

Abbildung 4: Nach der Installation des Graph-Algorithms-Plugins zeigt Neo4j die nachgeladenen Algorithmen an.
Doch es liegen noch mehr Steine im Weg. Beim Versuch, einen der Algorithmen tatsächlich zu verwenden, erklärt eine Fehlermeldung auf dem Schirm, dass dies aus sicherheitstechnischen Gründen in einem “Sandkasten” nicht möglich sei. Vielmehr sei es erforderlich, die importierten Algorithmen von den routinemäßig auferlegten Beschränkungen auszunehmen. Dazu müsse die Environment-Variable »NEO4J_dbms_security_procedures_unrestricted« mittels eines regulären Ausdrucks festlegen, dass alles unterhalb des Namensraums »algo« freie Bahn genießt.
Das Docker-Kommando in Abbildung 3 definiert die Variable bereits ordnungsgemäß. Auch setzt es die Variable »NEO4J_AUTH« auf »neo4j/test«, was den Server anweist, den sonst zwingend angeforderten Passwort-Reset zu unterlassen. Der Spaß kann beginnen!
Online PLUS
Im Screencast unter http://www.linux-magazin.de/videos/ demonstriert Michael Schilli das vorgestellte Programmierbeispiel.
Der Autor
Michael Schilli arbeitet als Software Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mailto:mschilli@perlmeister.com beantwortet er gern Fragen.
Infos
- Perl-Snapshot: Mike Schilli, “So’n Beziehungsding”, LM 06/2014, S. 102, https://www.lm-online.de/32328
- Similarity-Algorithmus in Neo4j:https://neo4j.com/docs/graph-algorithms/current/algorithms/node-similarity/
- Jaccard-Koeffizient: https://de.wikipedia.org/wiki/Jaccard-Koeffizient
- Algo-Plugin für Neo4j nachinstallieren:https://neo4j.com/docs/graph-algorithms/current/introduction/#_installation
- Listings zu diesem Artikel:http://www.linux-magazin.de/static/listings/magazin/2020/05/snapshot/









Sehr spaßiges Tool, man könnte auch jemanden fragen, der sich damit auskennt. Menschen können tatsächlich noch cleverer als Maschinen, Programme und Algorithmen sein. Tut mir echt leid. :(((
VG, content-werkstatt
Ich sehe hier ganz klar ein Problem, auf das hier nicht eingegangen ist. Der Algorithmus erkennt Worthülsen. D.h. wer keine kreative Rezension schreibt ist ein Betrüger? Wer von den ganzen Käufer besitzt schon eine Ausbildung im kreativen Schreiben oder technisches Verständnis um Sachverhalte auf den Punkt zu bringen? Also bestehen die Rezensionen zu 95% aus Worthülsen, so wie wir es Tag täglich aus der Werbung kennen. “Weißer als weiß” super alles toll und danke gerne wieder! Was soll man auch sonst schreiben? Der Verkäufer hält sich nicht an die gesetzliche Gewährleistung? Der Verkäufer interessiert sich nicht… Mehr »