Ein wesentlicher Bestandteil der OkCupid-Dating-Site ist die Empfehlung potenzieller Partner. Sie basieren auf der Überschneidung vieler Präferenzen, die Sie und Ihre potenziellen Partner angegeben haben. Wie Sie sich vorstellen können, gibt es viele Möglichkeiten, diese Aufgabe zu optimieren.
Ihre Präferenzen sind jedoch nicht der einzige Faktor, der beeinflusst, wen wir Ihnen als potenziellen Partner empfehlen (oder Sie selbst als potenziellen Partner für andere empfehlen). Wenn wir nur alle Benutzer anzeigen würden, die Ihren Kriterien entsprechen, ohne ein Ranking, wäre die Liste überhaupt nicht optimal. Wenn Sie beispielsweise die letzten Benutzeraktivitäten ignorieren, können Sie viel mehr Zeit damit verbringen, mit einer Person zu sprechen, die die Site nicht besucht. Zusätzlich zu den von Ihnen angegebenen Einstellungen verwenden wir zahlreiche Algorithmen und Faktoren, um Ihnen die Personen zu empfehlen, die Sie unserer Meinung nach sehen sollten.
Wir müssen die bestmöglichen Ergebnisse und eine fast endlose Liste von Empfehlungen liefern. In anderen Anwendungen, in denen sich der Inhalt weniger häufig ändert, können Sie dies tun, indem Sie die Empfehlungen regelmäßig aktualisieren. Wenn Sie beispielsweise die Spotify-Funktion "Wöchentlich entdecken" verwenden, genießen Sie eine Reihe empfohlener Titel. Diese Einstellung ändert sich erst nächste Woche. Auf OkCupid können Benutzer ihre Empfehlungen endlos in Echtzeit anzeigen. Der empfohlene „Inhalt“ ist sehr dynamisch (z. B. kann ein Benutzer seine Einstellungen, Profildaten, den Standort jederzeit ändern, deaktivieren usw.). Der Benutzer kann ändern, wer und wie er ihn empfehlen kann. Daher möchten wir sicherstellen, dass potenzielle Übereinstimmungen zu einem bestimmten Zeitpunkt die besten sind.
Um verschiedene Ranking-Algorithmen nutzen und Empfehlungen in Echtzeit abgeben zu können, müssen Sie eine Suchmaschine verwenden, die ständig mit Benutzerdaten aktualisiert wird und die Möglichkeit bietet, potenzielle Kandidaten zu filtern und zu bewerten.
Was sind die Probleme mit dem vorhandenen Match-Suchsystem?
OkCupid verwendet seit Jahren eine eigene interne Suchmaschine. Wir werden nicht auf Details eingehen, aber auf einer hohen Abstraktionsebene handelt es sich um ein kartenreduziertes Framework über User-Space-Shards, bei dem jeder Shard einige der relevanten Benutzerdaten im Speicher enthält, die verwendet werden, wenn verschiedene Filter und Sortierungen im laufenden Betrieb aktiviert werden. Die Suchbegriffe unterscheiden sich über alle Shards hinweg, und letztendlich werden die Ergebnisse kombiniert, um die Top-k-Kandidaten zurückzugeben. Dieses von uns geschriebene Pairing-System hat gut funktioniert. Warum haben wir uns entschieden, es jetzt zu ändern?
Wir wussten, dass wir das System aktualisieren mussten, um verschiedene empfehlungsbasierte Projekte in den kommenden Jahren zu unterstützen. Wir wussten, dass unser Team wachsen würde, ebenso wie die Anzahl der Projekte. Eine der größten Herausforderungen war die Aktualisierung des Schemas. Das Hinzufügen neuer Benutzerdaten (z. B. Gender-Tags in Einstellungen) erforderte beispielsweise Hunderte oder Tausende von Codezeilen in Vorlagen, und die Bereitstellung erforderte eine sorgfältige Koordination, um sicherzustellen, dass alle Teile des Systems in der richtigen Reihenfolge bereitgestellt wurden. Der Versuch, eine neue Methode zum Filtern eines benutzerdefinierten Datensatzes oder von Rangergebnissen hinzuzufügen, dauerte einen halben Tag. Er musste jedes Segment manuell in der Produktion bereitstellen und auf mögliche Probleme überwachen. Noch wichtiger ist, dass es schwierig geworden ist, das System zu verwalten und zu skalieren.weil Shards und Replikate manuell auf eine Flotte von Computern verteilt wurden, auf denen keine Software installiert war.
Zu Beginn des Jahres 2019 nahm die Belastung des Pairing-Systems zu, sodass wir einen weiteren Satz von Replikaten hinzufügten, indem wir Service-Instanzen manuell auf mehreren Computern platzierten. Die Arbeit im Backend und für die Entwickler dauerte viele Wochen. Während dieser Zeit stellten wir auch Leistungsengpässe bei der Erkennung eingebetteter Dienste, der Nachrichtenwarteschlange usw. fest. Obwohl diese Komponenten zuvor eine gute Leistung erbrachten, hatten wir einen Punkt erreicht, an dem wir anfingen, die Skalierbarkeit dieser Systeme in Frage zu stellen. Unsere Aufgabe war es, den größten Teil unserer Arbeitslast in die Cloud zu verlagern. Das Portieren dieses Pairing-Systems ist an sich schon eine mühsame Aufgabe, umfasst aber auch andere Subsysteme.
Heute werden bei OkCupid viele dieser Subsysteme mit robusteren und cloudfreundlicheren OSS-Optionen bedient, und das Team hat in den letzten zwei Jahren verschiedene Technologien mit großem Erfolg eingeführt. Wir werden hier nicht auf diese Projekte eingehen, sondern uns auf die Schritte konzentrieren, die wir unternommen haben, um die oben genannten Probleme zu lösen, und uns für unsere Empfehlungen einer entwicklerfreundlicheren und skalierbareren Suchmaschine zuwenden: Vespa .
Das ist ein Zufall! Warum OkCupid mit Vespa befreundet wurde
In der Vergangenheit war unser Team klein. Wir wussten von Anfang an, dass die Auswahl einer Suchmaschine äußerst schwierig sein würde, deshalb haben wir uns die Open-Source-Optionen angesehen, die für uns funktionieren. Die beiden Hauptkonkurrenten waren Elasticsearch und Vespa.
Elasticsearch
Es ist eine beliebte Technologie mit einer großen Community, guter Dokumentation und Unterstützung. Es gibt unzählige Funktionen und es wird sogar auf Tinder verwendet . Neue Schemafelder können mithilfe der PUT-Zuordnung hinzugefügt werden, Abfragen können mithilfe strukturierter REST-Aufrufe durchgeführt werden, es gibt Unterstützung für das Ranking nach Abfragezeit, die Möglichkeit, benutzerdefinierte Plugins zu schreiben usw. Wenn es um Skalierung und Wartung geht, müssen Sie nur die Anzahl der Shards definieren und das System selbst übernimmt die Replikatverteilung. Für die Skalierung muss ein weiterer Index mit mehr Shards neu erstellt werden.
Einer der Hauptgründe, warum wir Elasticsearch eingestellt haben, war das Fehlen echter Teilaktualisierungen im Speicher. Dies ist für unseren Anwendungsfall sehr wichtig, da die Dokumente, die wir indizieren möchten, aufgrund von Likes, Messaging usw. sehr häufig aktualisiert werden müssen. Diese Dokumente sind im Vergleich zu Inhalten wie Anzeigen oder Anzeigen sehr dynamisch Bilder, meist statische Objekte mit konstanten Attributen. Ineffiziente Lese- / Schreibzyklen bei Updates waren daher für uns ein großes Leistungsproblem.
Vespa
Der Quellcode wurde erst vor wenigen Jahren geöffnet. Die Entwickler kündigten Unterstützung für das Speichern, Suchen, Ranking und Organisieren von Big Data in Echtzeit an. Von Vespa unterstützte Funktionen:
Wenn es um Skalierung und Wartung geht, denken Sie nicht mehr an Shards - Sie richten das Layout für Ihre Inhaltsknoten ein und Vespa übernimmt automatisch das Sharden von Dokumenten, das Replizieren und Verteilen von Daten. Darüber hinaus werden Daten automatisch wiederhergestellt und aus Replikaten neu verteilt, wenn Sie Knoten hinzufügen oder entfernen. Skalieren bedeutet einfach, die Konfiguration zu aktualisieren, um Knoten hinzuzufügen, und ermöglicht es Vespa, diese Daten automatisch in Echtzeit neu zu verteilen.
Insgesamt schien Vespa für unsere Anwendungsfälle am besten geeignet zu sein. OkCupid enthält viele verschiedene Informationen über Benutzer, um ihnen zu helfen, die beste Übereinstimmung zu finden - nur in Bezug auf Filter und Sortierungen gibt es über hundert Parameter! Wir werden immer Filter und Sortierungen hinzufügen, daher ist es sehr wichtig, diesen Workflow beizubehalten. In Bezug auf Einträge und Abfragen ist Vespa unserem bestehenden System am ähnlichsten. Das heißt, unser System erforderte auch die Verarbeitung schneller Teilaktualisierungen im Speicher und die Echtzeitverarbeitung während einer Übereinstimmungsanforderung. Vespa hat auch eine viel flexiblere und einfachere Rangstruktur. Ein weiterer netter Bonus ist die Möglichkeit, Abfragen in YQL auszudrücken, im Gegensatz zu der unbequemen Struktur für Abfragen in Elasticsearch. In Bezug auf Skalierung und Wartung,Dann erwies sich die automatische Datenverteilung von Vespa für unser relativ kleines Team als sehr attraktiv. Insgesamt wurde festgestellt, dass Vespa unsere Anwendungsfälle und Leistungsanforderungen besser unterstützt und gleichzeitig einfacher zu warten ist als Elasticsearch.
Elasticsearch ist eine bekanntere Engine, und wir könnten von Tinders Erfahrung damit profitieren, aber jede Option würde eine Menge vorläufiger Forschung erfordern. Gleichzeitig bedient Vespa viele Produktionssysteme wie Zedge , Flickr mit Milliarden von Bildern und die Werbeplattform Yahoo Gemini Ads mit über hunderttausend Anfragen pro Sekunde, Anzeigen an 1 Milliarde aktive Nutzer pro Monat zu liefern. Dies gab uns das Vertrauen, dass es sich um eine kampferprobte, effiziente und zuverlässige Option handelte - tatsächlich gab es Vespa schon vor Elasticsearch.
Auch die Vespa-Entwickler haben sich als sehr kontaktfreudig und hilfreich erwiesen. Vespa wurde ursprünglich für Werbung und Inhalte entwickelt. Soweit wir wissen, wurde es noch nicht auf Dating-Sites verwendet. Anfangs war es schwierig, den Motor zu integrieren, da wir einen einzigartigen Anwendungsfall hatten, aber das Vespa-Team erwies sich als sehr reaktionsschnell und optimierte das System schnell, um uns bei der Lösung mehrerer auftretender Probleme zu unterstützen.
Wie Vespa funktioniert und wie die Suche in OkCupid aussieht
Bevor Sie in unser Vespa-Beispiel eintauchen, finden Sie hier einen kurzen Überblick über die Funktionsweise. Vespa ist eine Sammlung zahlreicher Dienste, aber jeder Docker-Container kann als Admin / Config-Host, als zustandsloser Java-Container-Host und / oder als Stateful C ++ - Inhaltshost konfiguriert werden. Anwendungspaket mit Konfiguration, Komponenten, ML-Modell usw. kann über die Status-API bereitgestellt werdenin einem Konfigurationscluster, der das Anwenden von Änderungen auf den Container und den Inhaltscluster behandelt. Feed-Anforderungen und andere Anforderungen durchlaufen einen zustandslosen Java-Container (der die Anpassung der Verarbeitung ermöglicht) über HTTP, bevor Feed-Aktualisierungen im Inhaltscluster eintreffen oder Anforderungen an die Inhaltsschicht weitergeleitet werden, in der die Ausführung verteilter Anforderungen erfolgt. Die Bereitstellung eines neuen Anwendungspakets dauert größtenteils nur wenige Sekunden, und Vespa verarbeitet diese Änderungen in Echtzeit im Container- und Inhaltscluster, sodass Sie selten etwas neu starten müssen.
Wie sieht die Suche aus?
Vespa-Clusterdokumente enthalten eine Vielzahl benutzerspezifischer Attribute. Die Schemadefinition definiert die Dokumenttypfelder sowie die Rangfolgeprofile , die den Satz der anwendbaren Rangfolgenausdrücke enthalten. Angenommen, wir haben eine Schemadefinition , die einen Benutzer wie diesen darstellt:
search user {
document user {
field userId type long {
indexing: summary | attribute
attribute: fast-search
rank: filter
}
field latLong type position {
indexing: attribute
}
# UNIX timestamp
field lastOnline type long {
indexing: attribute
attribute: fast-search
}
# Contains the users that this user document has liked
# and the corresponding weights are UNIX timestamps when that like happened
field likedUserSet type weightedset<long> {
indexing: attribute
attribute: fast-search
}
}
rank-profile myRankProfile inherits default {
rank-properties {
query(lastOnlineWeight): 0
query(incomingLikeWeight): 0
}
function lastOnlineScore() {
expression: query(lastOnlineWeight) * freshness(lastOnline)
}
function incomingLikeTimestamp() {
expression: rawScore(likedUserSet)
}
function hasLikedMe() {
expression: if (incomingLikeTimestamp > 0, 1, 0)
}
function incomingLikeScore() {
expression: query(incomingLikeWeight) * hasLikedMe
}
first-phase {
expression {
lastOnlineScore + incomingLikeScore
}
}
summary-features {
lastOnlineScore incomingLikeScore
}
}
}
Die Notation
indexing: attributegibt an, dass diese Felder im Speicher gespeichert werden sollten, um die beste Lese- und Schreibleistung dieser Felder zu erzielen.
Angenommen, wir haben den Cluster mit diesen benutzerdefinierten Dokumenten gefüllt. Wir könnten dann jedes der oben genannten Felder filtern und bewerten. Wenn Sie beispielsweise eine POST-Anfrage an die Standardsuchmaschine senden
http://localhost:8080/search/, um andere Benutzer als unseren eigenen Benutzer 777innerhalb von 50 Meilen von unserem Standort zu finden, die seit dem Zeitstempel online sind 1592486978, nach letzter Aktivität geordnet und die beiden besten Kandidaten behalten. Wählen wir auch die Zusammenfassungsfunktionen aus , um den Beitrag jedes Ranking-Ausdrucks in unserem Ranking-Profil anzuzeigen :
{
"yql": "select userId, summaryfeatures from user where lastOnline > 1592486978 and !(userId contains \"777\") limit 2;",
"ranking": {
"profile": "myRankProfile",
"features": {
"query(lastOnlineWeight)": "50"
}
},
"pos": {
"radius": "50mi",
"ll": "N40o44'22;W74o0'2",
"attribute": "latLong"
},
"presentation": {
"summary": "default"
}
}
Wir könnten ein Ergebnis wie dieses erzielen:
{
"root": {
"id": "toplevel",
"relevance": 1.0,
"fields": {
"totalCount": 317
},
"coverage": {
"coverage": 100,
"documents": 958,
"full": true,
"nodes": 1,
"results": 1,
"resultsFull": 1
},
"children": [
{
"id": "index:user/0/bde9bd654f1d5ae17fd9abc3",
"relevance": 48.99315843621399,
"source": "user",
"fields": {
"userId": -5800469520557156329,
"summaryfeatures": {
"rankingExpression(incomingLikeScore)": 0.0,
"rankingExpression(lastOnlineScore)": 48.99315843621399,
"vespa.summaryFeatures.cached": 0.0
}
}
},
{
"id": "index:user/0/e8aa37df0832905c3fa1dbbd",
"relevance": 48.99041280864198,
"source": "user",
"fields": {
"userId": 6888497210242094612,
"summaryfeatures": {
"rankingExpression(incomingLikeScore)": 0.0,
"rankingExpression(lastOnlineScore)": 48.99041280864198,
"vespa.summaryFeatures.cached": 0.0
}
}
}
]
}
}
Nach dem Filtern durch Matching-Ergebnis-Ranking berechneter Ausdruck der ersten Phase (erste Phase) zum Ranking der Ergebnisse. Die zurückgegebene Relevanz (Relevanz) ist die Gesamtbewertung als Ergebnis der Ausführung aller Ranking-Funktionen der ersten Phase des Ranking-Profils (Ranking-Profil), das wir in unserer Abfrage angegeben haben
ranking.profile myRankProfile. ranking.featuresWir definieren query(lastOnlineWeight)50 in der Liste , auf die dann durch den einzigen von uns verwendeten Rangfolgenausdruck verwiesen wird lastOnlineScore. Es verwendet eine integrierte Ranking-Funktion freshness , die eine Zahl nahe 1 ist, wenn der Zeitstempel im Attribut aktueller als der aktuelle Zeitstempel ist. Solange alles gut läuft, ist hier nichts kompliziert.
Im Gegensatz zu statischen Inhalten kann dieser Inhalt beeinflussen, ob er dem Benutzer angezeigt wird oder nicht. Zum Beispiel könnten sie dich mögen! Wir könnten ein gewichtetes Feld
likedUserSet für jedes Benutzerdokument indizieren , das als Schlüssel die IDs der Benutzer enthält, die sie mochten, und als Werte den Zeitstempel, wann dies geschah. Dann wäre es einfach, diejenigen herauszufiltern, die Sie mochten (z. B. einen Ausdruck likedUserSet contains \”777\”in YQL hinzufügen ), aber wie können diese Informationen beim Ranking berücksichtigt werden? Wie kann man den togr eines Benutzers erhöhen, der unsere Person in den Ergebnissen mochte?
In früheren Ergebnissen war der Rangausdruck
incomingLikeScorefür beide Treffer 0. Der Benutzer 6888497210242094612mochte den Benutzer tatsächlich777aber es ist derzeit nicht in der Rangliste verfügbar, selbst wenn wir gesetzt hatten "query(incomingLikeWeight)": 50. Wir können die Rangfunktion in YQL verwenden (das erste und einzige Argument der Funktion rank()bestimmt, ob das Dokument übereinstimmt, aber alle Argumente werden zur Berechnung der Rangfolge verwendet) und dann dotProduct in unserem YQL-Rangfolgenausdruck verwenden, um die Rohwerte zu speichern und abzurufen (in diesem Fall) Zeitstempel, wenn der Benutzer uns mochte), zum Beispiel auf folgende Weise:
{
"yql": "select userId,summaryfeatures from user where !(userId contains \"777\") and rank(lastOnline > 1592486978, dotProduct(likedUserSet, {\"777\":1})) limit 2;",
"ranking": {
"profile": "myRankProfile",
"features": {
"query(lastOnlineWeight)": "50",
"query(incomingLikeWeight)": "50"
}
},
"pos": {
"radius": "50mi",
"ll": "N40o44'22;W74o0'2",
"attribute": "latLong"
},
"presentation": {
"summary": "default"
}
}
{
"root": {
"id": "toplevel",
"relevance": 1.0,
"fields": {
"totalCount": 317
},
"coverage": {
"coverage": 100,
"documents": 958,
"full": true,
"nodes": 1,
"results": 1,
"resultsFull": 1
},
"children": [
{
"id": "index:user/0/e8aa37df0832905c3fa1dbbd",
"relevance": 98.97595807613169,
"source": "user",
"fields": {
"userId": 6888497210242094612,
"summaryfeatures": {
"rankingExpression(incomingLikeScore)": 50.0,
"rankingExpression(lastOnlineScore)": 48.97595807613169,
"vespa.summaryFeatures.cached": 0.0
}
}
},
{
"id": "index:user/0/bde9bd654f1d5ae17fd9abc3",
"relevance": 48.9787037037037,
"source": "user",
"fields": {
"userId": -5800469520557156329,
"summaryfeatures": {
"rankingExpression(incomingLikeScore)": 0.0,
"rankingExpression(lastOnlineScore)": 48.9787037037037,
"vespa.summaryFeatures.cached": 0.0
}
}
}
]
}
}
Jetzt wird der Benutzer
68888497210242094612nach oben angehoben, da er unseren Benutzer mochte und er incomingLikeScoredie volle Bedeutung hat. Natürlich haben wir tatsächlich einen Zeitstempel, als er uns mochte, damit wir ihn in komplexeren Ausdrücken verwenden können, aber lassen wir es vorerst einfach.
Dies demonstriert die Mechanik des Filterns und Rankings von Ergebnissen unter Verwendung eines Ranking-Systems. Das Ranking-Framework bietet eine flexible Möglichkeit, Ausdrücke (die meist nur mathematisch sind) auf Übereinstimmungen während einer Abfrage anzuwenden.
Middleware in Java einrichten
Was wäre, wenn wir einen anderen Weg einschlagen und diesen dotProduct-Ausdruck implizit zu jeder Anforderung machen möchten? Hier kommt die benutzerdefinierte Java-Containerebene ins Spiel - wir können eine benutzerdefinierte Sucherkomponente schreiben . Auf diese Weise können Sie beliebige Parameter verarbeiten, die Abfrage neu schreiben und die Ergebnisse auf eine bestimmte Weise verarbeiten. Hier ist ein Beispiel in Kotlin:
@After(PhaseNames.TRANSFORMED_QUERY)
class MatchSearcher : Searcher() {
companion object {
// HTTP query parameter
val USERID_QUERY_PARAM = "userid"
val ATTRIBUTE_FIELD_LIKED_USER_SET = “likedUserSet”
}
override fun search(query: Query, execution: Execution): Result {
val userId = query.properties().getString(USERID_QUERY_PARAM)?.toLong()
// Add the dotProduct clause
If (userId != null) {
val rankItem = query.model.queryTree.getRankItem()
val likedUserSetClause = DotProductItem(ATTRIBUTE_FIELD_LIKED_USER_SET)
likedUserSetClause.addToken(userId, 1)
rankItem.addItem(likedUserSetClause)
}
// Execute the query
query.trace("YQL after is: ${query.yqlRepresentation()}", 2)
return execution.search(query)
}
}
In unserer Datei services.xml können wir diese Komponente dann wie folgt konfigurieren:
...
<search>
<chain id="default" inherits="vespa">
<searcher id="com.okcupid.match.MatchSearcher" bundle="match-searcher"/>
</chain>
</search>
<handler id="default" bundle="match-searcher">
<binding>http://*:8080/match</binding>
</handler>
...
Dann erstellen und implementieren wir einfach das Anwendungspaket und stellen eine Anfrage an den benutzerdefinierten Handler
http://localhost:8080/match-?userid=777:
{
"yql": "select userId,summaryfeatures from user where !(userId contains \"777\") and rank(lastOnline > 1592486978) limit 2;",
"ranking": {
"profile": "myRankProfile",
"features": {
"query(lastOnlineWeight)": "50",
"query(incomingLikeWeight)": "50"
}
},
"pos": {
"radius": "50mi",
"ll": "N40o44'22;W74o0'2",
"attribute": "latLong"
},
"presentation": {
"summary": "default"
}
}
Wir erhalten die gleichen Ergebnisse wie zuvor! Beachten Sie, dass wir im Kotlin-Code einen Traceback hinzugefügt haben, um die YQL-Ansicht nach der Änderung auszugeben. Wenn dies
tracelevel=2in den URL-Parametern festgelegt ist, wird auch die Antwort angezeigt:
...
{
"message": "YQL after is: select userId, summaryfeatures from user where ((rank(lastOnline > 1592486978, dotProduct(likedUserSet, {\"777\": 1})) AND !(userId contains \"777\") limit 2;"
},
...
Der Java-Middleware-Container ist ein leistungsstarkes Tool zum Hinzufügen einer benutzerdefinierten Verarbeitungslogik über Searcher oder die native Generierung von Ergebnissen mit Renderer . Wir passen unsere Searcher- Komponenten anFälle wie die oben genannten und andere Aspekte zu behandeln, die wir in unsere Suche einbeziehen möchten. Eines der von uns unterstützten Produktkonzepte ist beispielsweise die Idee der "Reziprozität". Sie können nach Benutzern mit bestimmten Kriterien (wie Altersgruppe und Entfernung) suchen, müssen aber auch die Suchkriterien für Kandidaten erfüllen. Um dies in unserer Sucherkomponente zu unterstützen, können wir das Dokument des Benutzers abrufen, der sucht, um einige seiner Attribute in einer nachfolgenden gegabelten Abfrage zum Filtern und Ranking bereitzustellen. Das Ranking-Framework und die benutzerdefinierte Middleware bieten zusammen eine flexible Möglichkeit, mehrere Anwendungsfälle zu unterstützen. Wir haben in diesen Beispielen nur einige Aspekte behandelt, aber hier Hier finden Sie eine detaillierte Dokumentation.
Wie wir einen Vespa-Cluster aufgebaut und in Produktion genommen haben
Im Frühjahr 2019 haben wir mit der Planung eines neuen Systems begonnen. Während dieser Zeit haben wir auch das Vespa-Team kontaktiert und uns regelmäßig über unsere Anwendungsfälle beraten. Unser operatives Team bewertete und baute das erste Cluster-Setup auf und das Backend-Team begann mit der Dokumentation, dem Design und dem Prototyping verschiedener Vespa-Anwendungsfälle.
Die ersten Phasen des Prototyping
OkCupid-Backend-Systeme sind in Golang und C ++ geschrieben. Um benutzerdefinierte Vespa-Logikkomponenten zu schreiben und mithilfe der Java Vespa HTTP-Feed-Client-API hohe Vorschubraten bereitzustellen , mussten wir uns ein wenig mit der JVM-Umgebung vertraut machen. Bei der Konfiguration von Vespa-Komponenten und in unseren Feed-Pipelines verwendeten wir Kotlin.
Es dauerte mehrere Jahre, um die Anwendungslogik zu portieren und die Vespa-Funktionen zu enthüllen. Bei Bedarf wurde das Vespa-Team konsultiert. Der größte Teil der Systemlogik der Match-Engine ist in C ++ geschrieben. Daher haben wir auch Logik hinzugefügt, um unser aktuelles Filter- und Sortierdatenmodell in äquivalente YQL-Abfragen zu übersetzen, die wir über REST an den Vespa-Cluster senden. Schon früh haben wir uns darum gekümmert, eine gute Pipeline zu erstellen, um den Cluster mit einer vollständigen Benutzerbasis von Dokumenten neu zu füllen. Das Prototyping muss viele Änderungen beinhalten, um die richtigen zu verwendenden Feldtypen zu bestimmen, und erfordert versehentlich das erneute Senden des Dokumentvorschubs.
Überwachung und Stresstest
Bei der Erstellung des Vespa-Suchclusters mussten wir zwei Dinge sicherstellen: dass er das erwartete Volumen an Suchanfragen und Datensätzen verarbeiten kann und dass die Empfehlungen des Systems qualitativ mit dem vorhandenen Pairing-System vergleichbar sind.
Vor den Belastungstests haben wir überall Prometheus-Metriken hinzugefügt. Vespa-Exporter bietet unzählige Statistiken, und Vespa selbst bietet eine kleine Reihe zusätzlicher Metriken . Auf dieser Grundlage haben wir verschiedene Grafana-Dashboards für Anforderungen pro Sekunde, Latenz, Ressourcennutzung durch Vespa-Prozesse usw. erstellt. Außerdem haben wir vespa-fbench ausgeführt , um die Abfrageleistung zu testen. Mit Hilfe von Vespa-Entwicklern haben wir das aufgrund des relativ hohen festgestelltAufgrund der Kosten für statische Anforderungen liefert unser gruppiertes, vorgefertigtes Layout schnellere Ergebnisse. In einem flachen Layout reduziert das Hinzufügen weiterer Knoten im Grunde nur die Kosten einer dynamischen Abfrage (dh den Teil der Abfrage, der von der Anzahl der indizierten Dokumente abhängt). Ein gruppiertes Layout bedeutet, dass jede konfigurierte Standortgruppe einen vollständigen Satz von Dokumenten enthält und daher eine Gruppe die Anforderung bearbeiten kann. Aufgrund der hohen Kosten für statische Anforderungen haben wir bei gleichbleibender Anzahl von Knoten den Durchsatz erheblich erhöht und die Anzahl von einer flachen Gruppe auf drei erhöht. Schließlich haben wir auch nicht gemeldeten "Schattenverkehr" in Echtzeit getestet, als wir uns auf die Zuverlässigkeit statischer Benchmarks verlassen konnten.
Leistung optimieren
Die Leistung an der Kasse war eine der größten Hürden, mit denen wir schon früh konfrontiert waren. Zu Beginn hatten wir Probleme bei der Verarbeitung von Updates, selbst bei 1000 QPS (Anforderungen pro Sekunde). Wir haben häufig gewichtete Mengenfelder verwendet, aber sie waren zunächst nicht effektiv. Glücklicherweise halfen die Entwickler von Vespa schnell bei der Lösung dieser Probleme sowie anderer Probleme im Zusammenhang mit der Datenverbreitung. Später fügten sie auch eine umfangreiche Dokumentation zur Feed-Dimensionierung hinzu , die wir in gewissem Umfang verwenden: Ganzzahlige Felder in großen gewichteten Mengen ermöglichen, wenn möglich, das Stapeln nach Einstellung
visibility-delaydurch die Verwendung mehrerer bedingter Aktualisierungen und das Verlassen auf Attributfelder (dh im Speicher) sowie durch die Reduzierung der Anzahl von Roundtrip-Paketen von Clients durch Komprimieren und Zusammenführen von Vorgängen in unseren fmdov-Pipelines. Jetzt verarbeiten Pipelines im stationären Zustand leise 3000 QPS, und unser bescheidener Cluster verarbeitet 11.000 QPS-Aktualisierungen, wenn aus irgendeinem Grund eine solche Spitze auftritt.
Qualität der Empfehlungen
Nachdem wir überzeugt waren, dass der Cluster die Last bewältigen kann, musste überprüft werden, ob die Qualität der Empfehlungen nicht schlechter ist als im vorhandenen System. Jede geringfügige Abweichung bei der Umsetzung des Ratings hat enorme Auswirkungen auf die Gesamtqualität der Empfehlungen und das gesamte Ökosystem insgesamt. Wir haben ein experimentelles System angewendetVespa in einigen Testgruppen, während die Kontrollgruppe das vorhandene System weiterhin verwendete. Anschließend wurden mehrere Geschäftsmetriken analysiert, wobei Probleme wiederholt und behoben wurden, bis die Vespa-Gruppe eine ebenso gute, wenn nicht sogar bessere Leistung erbrachte als die Kontrollgruppe. Sobald wir von den Vespa-Ergebnissen überzeugt waren, war es einfach, Match-Anfragen an den Vespa-Cluster weiterzuleiten. Wir konnten den gesamten Suchverkehr problemlos in den Vespa-Cluster starten!
Systemdiagramm
In vereinfachter Form sieht das endgültige Architekturdiagramm des neuen Systems folgendermaßen aus:
Wie Vespa jetzt funktioniert und wie es weitergeht
Vergleichen wir den Status des Vespa-Pair-Finders mit dem vorherigen System:
- Schemaaktualisierungen
- Vorher: eine Woche mit Hunderten neuer Codezeilen, sorgfältig koordinierte Bereitstellungen mit mehreren Subsystemen
- :
- Vorher: eine Woche mit Hunderten neuer Codezeilen, sorgfältig koordinierte Bereitstellungen mit mehreren Subsystemen
- /
- :
- : . , !
- :
-
- : ,
- : , Vespa . -
- : ,
Insgesamt hat der Design- und Wartungsaspekt des Vespa-Clusters zur Entwicklung aller OkCupid-Produkte beigetragen. Ende Januar 2020 haben wir unseren Vespa-Cluster in Produktion genommen und er dient allen Empfehlungen bei der Suche nach Paaren. Wir haben außerdem Dutzende neuer Felder, Rangfolgenausdrücke und Anwendungsfälle hinzugefügt, die alle neuen Funktionen dieses Jahres unterstützen, z. B. Stacks . Und im Gegensatz zu unserem vorherigen Matchmaking-System verwenden wir jetzt Echtzeit-Modelle für maschinelles Lernen zur Abfragezeit.
Was weiter?
Für uns ist einer der Hauptvorteile von Vespa die direkte Unterstützung für das Ranking mithilfe von Tensoren und die Integration in Modelle, die mit Frameworks wie TensorFlow trainiert wurden . Dies ist eines der Hauptmerkmale, die wir in den kommenden Monaten entwickeln werden. Wir verwenden bereits Tensoren für einige Anwendungsfälle und werden in Kürze verschiedene Modelle für maschinelles Lernen integrieren, von denen wir hoffen, dass sie die Ergebnisse und Übereinstimmungen für unsere Benutzer besser vorhersagen.
Darüber hinaus hat Vespa kürzlich die Unterstützung mehrdimensionaler Indizes für den nächsten Nachbarn angekündigt, die vollständig in Echtzeit verfügbar, gleichzeitig durchsuchbar und dynamisch aktualisiert sind. Wir sind sehr daran interessiert, andere Anwendungsfälle für die Echtzeitsuche nach dem nächsten Nachbarn zu untersuchen .
OkCupid und Vespa. Gehen!
Viele Leute haben Elasticsearch gehört oder mit ihm gearbeitet, aber es gibt keine so große Community um Vespa. Wir glauben, dass viele andere Elasticsearch-Anwendungen auf Vespa besser funktionieren würden. Es ist großartig für OkCupid und wir sind froh, dass wir darauf umgestellt haben. Diese neue Architektur ermöglichte es uns, neue Funktionen viel schneller zu entwickeln. Wir sind ein relativ kleines Unternehmen, daher ist es großartig, sich nicht zu viele Sorgen über die Komplexität des Service zu machen. Wir sind jetzt viel besser darauf vorbereitet, unsere Suchmaschine zu skalieren. Ohne Vespa hätten wir sicherlich nicht die Fortschritte machen können, die wir im vergangenen Jahr gemacht haben. Weitere Informationen zu den technischen Funktionen von Vespa finden Sie in den Vespa AI in den E-Commerce- Richtlinien von @jobergum .
Wir haben den ersten Schritt gemacht und die Vespa-Entwickler gemocht. Sie schickten uns eine Nachricht zurück und es stellte sich als Zufall heraus! Ohne die Hilfe des Vespa-Teams hätten wir das nicht geschafft. Besonderer Dank geht an @jobergum und @geirst für Empfehlungen zum Ranking und zur Abfragebehandlung sowie an @kkraune und @vekterli für ihre Unterstützung. Das Maß an Unterstützung und Aufwand, das uns das Vespa-Team gegeben hat, war wirklich erstaunlich - von tiefen Einsichten in unseren Anwendungsfall über die Diagnose von Leistungsproblemen bis hin zu sofortigen Verbesserungen der Vespa-Engine. Genosse @vekterli flog sogar in unser New Yorker Büro und arbeitete eine Woche lang direkt mit uns zusammen, um die Integration des Motors zu unterstützen. Vielen Dank an das Vespa-Team!
Zusammenfassend haben wir nur einige Aspekte der Vespa-Nutzung angesprochen, aber ohne die enorme Arbeit unserer Backend- und Betriebsteams im vergangenen Jahr wäre dies nicht möglich gewesen. Wir standen vor vielen einzigartigen Herausforderungen, um die Lücke zwischen vorhandenen Systemen und dem moderneren Technologie-Stack zu schließen. Dies sind jedoch Themen für andere Artikel.