Profile ausfüllen ist wichtig, aber unsexy

Vorschläge aus dem Skillgraph: Ein Neo4j Microservice als Alternative zur Recommendation Engine
Keine Kommentare

Je mehr Daten Recruitern zur Verfügung stehen, desto schneller kann der optimale Job gefunden werden. Da das Ausfüllen von Profilen mittels den richtigen Vorschlägen leichter und schneller von der Hand geht, zeigt dieser Artikel, wie Sie Bewerbern eine automatisierte Liste an wahrscheinlichen Skills zur Verfügung stellen können.

Im modernen IT-Recruiting dreht sich viel um Geschwindigkeit. Geschmeidige Bedienung und schnelles Feedback werden erwartet, aber auf der anderen Seite ist die Suche nach dem besten Match zwischen Jobsuchenden und interessanten Dev-Jobs eine Suche nach der Nadel im Heuhaufen. Besonders wenn Entwicklerinnen und Entwickler in neue Technologien oder Branchen einsteigen wollen, also bei den richtig interessanten Matches, ist es essenziell, ein gut ausgefülltes Profil zur Hand zu haben. Eines, in dem auch Skills stehen, die nur so nebenbei gelernt wurden.

Im Registrierungsprozess müssen also möglichst detailliert die technischen Skills abgefragt und von Alltagstool bis Nischentechnologie erfasst werden. Nur: Wer hat schon Lust, durch eine Liste aller bekannten (zumindest matched.io bekannten) Tech-Skills zu blättern, um die paar Skills zu finden, die einem nicht als relevant eingefallen sind? Live-Suche mit Autovervollständigung schafft hier schon etwas Erleichterung, aber für eine wirklich gute User Experience muss etwas Smarteres her.

© matched.io

Wenn wir aus der Masse an technischen Skills Vorhersagen machen könnten, was Entwickler und Entwicklerin noch können könnten und diese als relevante Vorschläge anbieten, wäre das eine Verbesserung an beiden Enden.

© matched.io

Aber wie errät man laufend und personenbezogen, was eine Person für weitere Kenntnisse besitzt? In diesem Artikel beschreiben wir einen Ansatz, der schon ausgefüllte Skillsets, ein Graph Theorie Model und Neo4j verwendet, um möglichst treffend, aber auch schnell, solche Vorschläge zu generieren.

Devs, die dieses Skillset haben, können auch…

Amazons “Kunden, die diesen Artikel gekauft haben, kauften auch”-Funktion liefert hier eine gute Vorlage. Wir haben bereits viele ausgefüllte Profile, bei denen wir übliche Häufigkeiten von Skills errechnen können, um daraus dann Vorschläge abzuleiten. Eine statische Liste an “beliebten” Skills wird allerdings dem Anspruch auf Personenbezug nicht gerecht. Idealerweise sollte man an den bereits eingetragenen Skills erkennen können, um welche Art Entwickler bzw. Entwicklerin es sich handelt und beispielsweise zum Skillset [‘CSS’, ‘HTML’] eher JavaScript vorschlagen als Kafka. Um eine Heuristik für “Devs, die dieses Skillset haben, können auch…” zu bauen, bietet es sich also an, die Häufigkeit von Skill-Kombinationen als Grundlage für Vorschläge zu nehmen.

Modellierung mit Graph Sets

Wir haben also einen Datensatz an Profilen mit einem Set an beherrschten Skills und wollen daraus – für ein neues Set an Skills – einen sinnvollen Vorschlag machen, welcher Skill oft zusammen mit diesen Skills gekonnt wird. Ein Weg, diese Fragestellung zu modellieren, ist ein Graph-Ansatz. Dabei wird aus einem Datenset ein Graph gebaut, in dem alle Entwickler und Entwicklerinnen sowie alle Skills als nodes dargestellt werden. Danach wird jeder Dev-node über relationships mit genau den Skill-nodes, die sich im Skillset der Person befinden, verbunden.

© matched.io

Für einzelne Skills lässt sich hier schon ein intuitiv sinnvoller Vorschlag ablesen. Einer Person, die HTML beherrscht, würde man zum Beispiel zuerst Angular vorschlagen, da 2 Profile sowohl Angular als auch HTML enthalten, als weitere Vorschläge kämen Java und PHP in Frage. Wir berechnen also die Anzahl der Pfade [Skill1]-[Dev]-[Skill2], die im Datensatz vorkommen und speichern diesen Wert als Häufigkeit eines Skillpaares ab.

© matched.io

Bekommt man ein Skillset mit mehreren Skills, zu dem Vorschläge erstellt werden sollen, muss sich die Frage gestellt werden, wie wichtig es ist, dass das gesamte Skillset in einem Entwicklerprofil vorkommt, bevor Vorschläge daraus abgeleitet werden. So ist es zum Beispiel vorstellbar, für eine Entwicklerin, die schon C++ und Java angegeben hat, erst C vorzuschlagen und danach erst Angular. Einfach weil das Profil von Entwicklerin 1 wirklich beide Skills C++ und Java enthält. Wir haben uns stattdessen dafür entschieden, uns nur auf Skillpaare zu konzentrieren, um auch Fullstack Developern und anderen Profilen, die nicht so einfach in vorgeschriebene Boxen passen, sinnvolle Vorschläge präsentieren zu können. Wir werden also den Wert eines Vorschlagskills berechnen, indem wir die Summe der Häufigkeit aller Skillpaare (Vorschlagsskill, Skillsetskill) für alle Skills im Skillset summieren.

© matched.io

Mit diesem Modellziel im Kopf brauchen wir nun ein gutes Tool, um unseren Graph zu implementieren und Vorschläge schnell zu errechnen.

Graph in Neo4j erstellen

Der Kenner wird schon an den Bildern erkannt haben, dass wir unseren Graphen in Neo4j bauen werden. Das ist nicht nur übersichtlicher, da Neo4j als Graph Datenbank visuelle Darstellungstools bietet, die uns helfen, unsere Daten zu verstehen und Fehler zu finden, sondern wir werden sehen, dass sich auch die Häufigkeitsberechnung in Neo4j stark vereinfachen lässt. Zuerst wollen wir allerdings unsere Profildaten in Neo4j als Graph anlegen. Dafür erstellen wir die Entwickler-nodes und Skill-nodes über cypher calls in Neo4j:

//create developer nodes

CREATE (d1:Dev{name: 'dev1'})

CREATE (d2:Dev{name: 'dev2'})

CREATE (d3:Dev{name: 'dev3'})

CREATE (d4:Dev{name: 'dev4'})

//create skill nodes

CREATE (s1:Skill{name:'c'})

CREATE (s2:Skill{name:'c++'})

CREATE (s3:Skill{name:'java'})

CREATE (s4:Skill{name:'angular'})

CREATE (s5:Skill{name:'html'})

CREATE (s6:Skill{name:'php'})

Danach fügen wir die relationships hinzu, die repräsentieren, wer welche Skills beherrscht:

//create knowledge relationships

CREATE (d1)-[:KNOWS]->(s1)

CREATE (d1)-[:KNOWS]->(s2)

CREATE (d1)-[:KNOWS]->(s3)

CREATE (d2)-[:KNOWS]->(s3)

CREATE (d2)-[:KNOWS]->(s4)

CREATE (d3)-[:KNOWS]->(s3)

CREATE (d3)-[:KNOWS]->(s4)

CREATE (d3)-[:KNOWS]->(s5)

CREATE (d4)-[:KNOWS]->(s4)

CREATE (d4)-[:KNOWS]->(s5)

CREATE (d4)-[:KNOWS]->(s6)

Und erreichen dann mit einer einfachen Query das Beispielbild, das wir schon vorher gesehen haben, nur diesmal nicht so schön angeordnet (da hat der Autor glatt gemogelt und per Hand arrangiert):

//Show graph

MATCH (n) return n;

© matched.io

Preprocess für die Übersicht

Jetzt sind wir eigentlich bereit, einen cypher Query zu formulieren, der uns zu einem Skillset die Vorschlagsskills sortiert und nach Häufigkeit der Skillpaare ausgibt. Allerdings müssten wir dafür immer über die Dev-nodes gehen und die Anzahl der Pfade von Skill 1 über einen Dev zu Skill 2 im Query zählen. Da ist es doch übersichtlicher, die Häufigkeit, in der ein Skillpaar im gesamten Datensatz vorkommt, zunächst in einem Preprocessing Schritt zu berechnen und dann in folgenden Queries mit diesen errechneten Häufigkeiten zu arbeiten. Besonders wenn man mehrere Vorschläge aus dem selben Datenstand berechnen möchte, lohnt sich das Preprocessing, da die folgenden Queries dadurch schneller werden.

Unser preprocess-Schritt soll also für jedes Skillpaar im Graphen errechnen, wie viele Dev-nodes mit beiden Skill-nodes verbunden sind, also die Anzahl Pfade von Skill 1 über einen Dev zu Skill 2. In Neo4j syntax schreibt man so einen Pfad als (s1:Skill)-[:KNOWS]-(:Dev)-[:KNOWS]-(s2:Skill) und ein cypher Query, der diese Pfade zählt und das Ergebnis in eine relationship zwischen Skill 1 und Skill 2 als weight speichert. Dies sieht dann wie folgt aus:

//create skill-pair edges from dev paths

MATCH path=((s1:Skill)-[:KNOWS]-(:Dev)-[:KNOWS]-(s2:Skill))

WITH s1, s2, Count(path) as amount

MERGE (s1)-[p:SKILL_PAIR]-(s2)

SET p.weight = amount

Ab jetzt brauchen wir die Dev-nodes und KNOWS-relationships nicht mehr beachten und können mit Skill-nodes und SKILL_PAIR-relationships allein Vorschläge errechnen. Unser Graph ist damit übersichtlicher, schneller und intuitiv verständlicher geworden:

//Show skillgraph

MATCH (n:Skill) return n;

© matched.io

Man beachte, dass die Richtungen der SKILL_PAIR-relationships keine Bedeutung haben. Bei den KNOWS-relationships ergab der Pfeil noch Sinn (auch wenn wir die Richtung nicht gebraucht haben), aber hier, bei SKILL_PAIR, wird die Richtung arbiträr durch die Reihenfolge der Skills in unserer Query gesetzt. Neo4j erlaubt leider keine echten undirected relationships, man muss als Entwickler bzw. Entwicklerin also im Kopf haben, für welche Richtungen man sich interessiert und welche keinen Wert haben. Wir werden in den Match Queries in diesem Artikel alle relationship-Richtungen ignorieren, indem wir, wie oben schon, (:Dev)-[:KNOWS]-(s2:Skill) statt (:Dev)-[:KNOWS]->(s2:Skill) verwenden – ohne Pfeil, der die Richtung vorgibt. Auf diese Weise sucht Neo4j nun nach einer beliebig gerichteten relationship.

Closest Skill Query

Bevor wir uns den Vorschlägen zu Skillsets widmen, wollen wir erstmal sehen, wie eine Query aussieht, die zu einem einzelnen Skill einen Vorschlag liefert. Dafür durchsuchen wir mit MATCH alle SKILL_PAIR-relationships, die unseren Startskill s1 mit einem Vorschlagsskill s2 verbinden und sortieren die so erreichten Skills nach Gewicht der relationship:

//search closest single

MATCH (s1:Skill)-[r:SKILL_PAIR]-(s2:Skill)

WHERE s1.name=’html’

RETURN s2.name, r.weightORDER BY r.weight DESC

© matched.io

Das sieht schonmal vielversprechend und bisher sehr übersichtlich aus. Für den nächsten Schritt brauchen wir jetzt das Konzept eines Skillsets, das wir in diesem Beispiel anhand einer Liste an Skillnamen zusammensuchen und dann im MATCH verwenden wollen. Zum Zusammensuchen des Skillsets verwenden wir wieder MATCH und speichern das Ergebnis dann für die weitere Verwendung in der Query über “as skill_nodes” ab:

WITH ['c', 'java'] as skills

MATCH (st:Skill) WHERE st.name IN skills

WITH collect(st) as skill_nodes

Generell werden wir viel “as xyz” verwenden, um in unserer Query Werte weiterzureichen oder nur verständlicher zu benennen. Mit den relevanten skill_nodes in der Hand können wir jetzt die Query fortsetzen und per MATCH genau die SKILL_PAIR-relationships finden, die einen Skill in unserem Skillset mit einem Skill von außerhalb verbinden (wir wollen ja nicht Skills vorschlagen, die der Entwickler oder die Entwicklerin bereits eingetragen hat):

MATCH (s1:Skill)-[r:SKILL_PAIR]-(s2:Skill)

WHERE s1 IN skill_nodes AND NOT s2 IN skill_nodes

Insgesamt sieht die cypher Query, die unsere Vorschläge liefert, dann so aus:

//search closest

WITH ['c', 'java'] as skills

MATCH (st:Skill) WHERE st.name IN skills

WITH collect(st) as skill_nodes

MATCH (s1:Skill)-[r:SKILL_PAIR]-(s2:Skill)

WHERE s1 IN skill_nodes AND NOT s2 IN skill_nodes

RETURN s2.name as suggested_skill, SUM(r.weight) as fit

ORDER BY fit DESC

© matched.io

Ergebnisse aus dem Microservice anfragen

Mit ein paar schönen Queries und einer manuell angefragten Lösung ist es natürlich nicht getan. Sowohl das Erstellen des Häufigkeitsgraphen im Preprocessing und die Vorschlagsabfrage sollte häufig und automatisch und im Falle der Vorschlagsabfrage sogar quasi live durchführbar sein. Zum Glück stellt Neo4j hier ein cypher-API zur Verfügung, das von recht vielen Sprachen aus über bereitgestellte Packages ansprechbar ist. Für unser Problem erwarten wir viele winzige Anfragen (nach jedem ausgewählten Skill fürs Skillset soll ein neues Vorschlagsset berechnet werden) und haben uns daher für einen Go Microservice entschieden.

Da wir sehr viele sehr zeitkritische kleine Anfragen nach Vorschlägen erwarten, haben wir die Berechnung des Häufigkeitsgraphen als Preprocessing vom Vorschlagsquery getrennt. Dieser Häufigkeitsgraph bildet auf Basis unserer Profildaten eine beste Schätzung, wie oft Skillpaare zusammen gekonnt werden. Er wird regelmäßig neu berechnet, um Änderungen und neue Profile einzufangen, da sich die Häufigkeiten von Skillpaaren aber eher langsam ändern und die Berechnung durch alle Daten fast eine Stunde dauert, updaten wir nur einmal die Woche. Wir müssen etwas im Auge behalten, dass wir natürlich durch die Einschränkung auf schon bekannte Profile einen Bias in unseren Vorschlägen haben und damit wir nur weiter vorschlagen, was wir schon als Kombination kennen. Nutzen alle nur noch die Skillvorschläge in der Profilerstellung und vergessen dadurch ihre seltenen Skillkombinationen, könnte ein zu erfolgreiches Vorschlags-Feature die Profile negativ beeinflussen. Lösungsideen werden im Fazit angerissen.

Erstmal haben wir einen gespeicherten Häufigkeitsgraph, der unsere beste Schätzung der Wirklichkeit enthält, dieser liegt im Neo4j Server in der Cloud und ist bereit, für Queries genutzt zu werden. In Go spricht man Neo4j über den neo4j-go-driver an und arbeitet hier recht direkt mit cypher queries. Wollen wir jetzt unsere Vorschlagsquery mit einem Skillset als Argument aufrufen, sieht das in etwa so aus (diese Code Beispiele sind nur Schnipsel, die den Arbeitsfluss darstellen sollen):

// define the query with variable skillset

query := `WITH $array as skills

MATCH (st:Skill) WHERE st.id IN skills

WITH collect(st) as skill_nodes

MATCH (s1:Skill)-[r:SKILL_PAIR]-(s2:Skill)

WHERE s1 IN skill_nodes AND not s2 in skill_nodes

RETURN s2.name as name, s2.id as id, SUM(r.weight) as fit

ORDER by fit DESC

LIMIT 50`

// map skillset to name in query

mappings := map[string]interface{}{

"array": request.Skills,}

// run query against open neo4j sessionskills, err = neo4j.Collect(session.ReadTransaction(func(tx neo4j.Transaction) (interface{},

error) {

return tx.Run(query, mappings)}

Eine relativ direkte Implementation der API-Schnittstelle, aber für unsere Zwecke genau richtig, da wir die cypher queries schon erarbeitet hatten. Dieser Ansatz in einem Go Microservice verbaut lässt uns schnell, ausreichend aktuell und vor allem parallel die Vorschlagsanfragen abarbeiten.

Fazit

Graphdarstellungen eignen sich oft, um Problemstellungen visuell verständlicher zu machen und mit Hilfe von Neo4j Queries kommt man auch schnell zu Ergebnissen, ohne Graph Algorithmen implementieren zu müssen. Damit erschließt man sich pragmatisch Informationen aus dem eigenen Datenschatz, ohne für eine simple Vorschlag-Engine gleich Blackbox-Technologien wie Deep Learning auspacken zu müssen oder seine Anforderungen an den Fokus bestehender Recommendation Systems anzupassen, wie man sie aus dem eCommerce Sektor kennt.

Für weitere Spielereien, wie das automatische Erkennen der anfangs erwähnten “Typen” von Entwicklern und Entwicklerinnen, bietet Neo4j Implementationen von Community-Detection-Algorithmen wie zum Beispiel die Louvain Methode, mit der wir in unserem Skillgraphen die eng verbundenen Communities identifizieren könnten, um dann damit zum Beispiel seltene Skillkombinationen aus verschiedenen Communities anbieten zu können. Dies wäre eine Strategie zum Ausbruch aus dem Bias der eigenen Daten, aber wie das Lehrbuch so schön sagt: “This is left as an exercise for the reader”.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Inline Feedbacks
View all comments
X
- Gib Deinen Standort ein -
- or -