La recommandation de partenaires potentiels fait partie intégrante du site de rencontre OkCupid. Ils sont basés sur le chevauchement de nombreuses préférences que vous et vos partenaires potentiels avez indiquées. Comme vous pouvez l'imaginer, il existe de nombreuses façons d'optimiser cette tâche.
Cependant, vos préférences ne sont pas le seul facteur qui influence la personne que nous vous recommandons en tant que partenaire potentiel (ou vous recommandez vous-même en tant que partenaire potentiel pour les autres). Si nous devions simplement afficher tous les utilisateurs qui correspondent à vos critères, sans aucun classement, alors la liste ne serait pas optimale du tout. Par exemple, si vous ignorez l'activité récente des utilisateurs, vous pouvez passer beaucoup plus de temps à parler à une personne qui ne visite pas le site. En plus des préférences que vous spécifiez, nous utilisons de nombreux algorithmes et facteurs pour vous recommander les personnes que nous pensons que vous devriez voir.
Nous devons fournir les meilleurs résultats possibles et une liste presque infinie de recommandations. Dans d'autres applications, où le contenu change moins fréquemment, cela peut être fait en mettant à jour périodiquement les recommandations. Par exemple, lorsque vous utilisez la fonction «Discover Weekly» de Spotify, vous profitez d'un ensemble de pistes recommandées, cet ensemble ne change pas avant la semaine prochaine. Sur OkCupid, les utilisateurs visualisent à l'infini leurs recommandations en temps réel. Le «contenu» recommandé est de nature très dynamique (par exemple, un utilisateur peut modifier ses préférences, ses données de profil, son emplacement, le désactiver à tout moment, etc.). L'utilisateur peut changer qui et comment il peut le recommander, nous voulons donc nous assurer que les correspondances potentielles sont les meilleures à un moment donné.
Pour tirer parti des différents algorithmes de classement et faire des recommandations en temps réel, vous devez utiliser un moteur de recherche constamment mis à jour avec les données des utilisateurs et offrant la possibilité de filtrer et de classer les candidats potentiels.
Quels sont les problèmes avec le système de recherche de correspondance existant
OkCupid utilise son propre moteur de recherche interne depuis des années. Nous n'entrerons pas dans les détails, mais à un niveau d'abstraction élevé, il s'agit d'un cadre de réduction de carte sur des fragments d'espace utilisateur, où chaque fragment contient certaines des données utilisateur pertinentes en mémoire, qui sont utilisées lors de l'activation de divers filtres et tris à la volée. Les termes de recherche divergent dans tous les fragments et, finalement, les résultats sont combinés pour renvoyer les k meilleurs candidats. Ce système d'appariement que nous avons écrit fonctionnait bien, alors pourquoi avons-nous décidé de le changer maintenant?
Nous savions que nous devions mettre à jour le système pour prendre en charge divers projets fondés sur des recommandations dans les années à venir. Nous savions que notre équipe grandirait, tout comme le nombre de projets. L'un des plus grands défis était la mise à jour du schéma. Par exemple, l'ajout d'un nouvel élément de données utilisateur (par exemple, des balises de genre dans les préférences) nécessitait des centaines ou des milliers de lignes de code dans les modèles, et le déploiement nécessitait une coordination minutieuse pour s'assurer que toutes les parties du système étaient déployées dans le bon ordre. Le simple fait d'essayer d'ajouter une nouvelle façon de filtrer un ensemble de données personnalisé ou de classer les résultats a pris une demi-journée de temps d'ingénieur. Il devait déployer manuellement chaque segment en production et surveiller les problèmes potentiels. Plus important encore, il est devenu difficile de gérer et de faire évoluer le système,car les fragments et les répliques étaient distribués manuellement sur un parc de machines sur lequel aucun logiciel n'était installé.
Début 2019, la charge sur le système de couplage a augmenté, nous avons donc ajouté un autre ensemble de répliques en plaçant manuellement des instances de service sur plusieurs machines. Le travail a pris plusieurs semaines sur le backend et pour les devops. Pendant ce temps, nous avons également commencé à remarquer des goulots d'étranglement dans la découverte de services intégrés, la mise en file d'attente des messages, etc. Alors que ces composants fonctionnaient auparavant bien, nous avions atteint un point où nous avons commencé à remettre en question l'évolutivité de ces systèmes. Nous avions la tâche de transférer la majeure partie de notre charge de travail vers le cloud. Le portage de ce système d'appariement est une tâche fastidieuse en soi, mais cela implique également d'autres sous-systèmes.
Aujourd'hui, chez OkCupid, bon nombre de ces sous-systèmes sont servis par des options OSS plus robustes et plus conviviales pour le cloud, et l'équipe a adopté diverses technologies avec un grand succès au cours des deux dernières années. Nous n'entrerons pas dans ces projets ici, mais nous nous concentrerons plutôt sur les mesures que nous avons prises pour résoudre les problèmes ci-dessus, en passant à un moteur de recherche plus convivial et évolutif pour nos recommandations: Vespa .
C'est une coïncidence! Pourquoi OkCupid est devenu ami avec Vespa
Historiquement, notre équipe était petite. Nous savions dès le départ que choisir un noyau de moteur de recherche serait extrêmement difficile, nous avons donc examiné les options open source qui fonctionnaient pour nous. Les deux principaux prétendants étaient Elasticsearch et Vespa.
Elasticsearch
C'est une technologie populaire avec une grande communauté, une bonne documentation et un support. Il y a des tonnes de fonctionnalités et il est même utilisé sur Tinder . De nouveaux champs de schéma peuvent être ajoutés à l'aide du mappage PUT, les requêtes peuvent être effectuées à l'aide d'appels REST structurés, il existe un support pour le classement par heure de requête, la possibilité d'écrire des plugins personnalisés, etc. En ce qui concerne la mise à l'échelle et la maintenance, il vous suffit de définir le nombre de fragments et le système lui-même gère la distribution des répliques. La mise à l'échelle nécessite la reconstruction d'un autre index avec plus de fragments.
L'une des principales raisons pour lesquelles nous avons abandonné Elasticsearch était le manque de véritables mises à jour partielles de la mémoire. Ceci est très important pour notre cas d'utilisation, car les documents que nous allons indexer doivent être mis à jour très souvent en raison de likes, de messages, etc. Ces documents sont de nature très dynamique, par rapport à des contenus comme des publicités ou images, qui sont pour la plupart des objets statiques avec des attributs constants. Par conséquent, les cycles de lecture-écriture inefficaces sur les mises à jour étaient un problème de performances majeur pour nous.
Vespa
Le code source a été ouvert il y a seulement quelques années. Les développeurs ont annoncé la prise en charge du stockage, de la recherche, du classement et de l'organisation du Big Data en temps réel. Fonctionnalités prises en charge par Vespa:
En ce qui concerne la mise à l'échelle et la maintenance, vous ne pensez plus aux fragments - vous configurez la mise en page de vos nœuds de contenu et Vespa gère automatiquement comment fragmenter les documents, répliquer et distribuer les données. De plus, les données sont automatiquement restaurées et redistribuées à partir des réplicas chaque fois que vous ajoutez ou supprimez des nœuds. La mise à l'échelle signifie simplement mettre à jour la configuration pour ajouter des nœuds et permet à Vespa de redistribuer automatiquement ces données en temps réel.
Dans l'ensemble, Vespa semblait correspondre le mieux à nos cas d'utilisation. OkCupid comprend de nombreuses informations différentes sur les utilisateurs pour les aider à trouver la meilleure correspondance - en termes de filtres et de tri, il existe plus d'une centaine de paramètres! Nous ajouterons toujours des filtres et des tris, il est donc très important de maintenir ce flux de travail. En termes d'entrées et de requêtes, Vespa est le plus similaire à notre système existant; autrement dit, notre système nécessitait également le traitement de mises à jour partielles rapides en mémoire et un traitement en temps réel lors d'une demande de correspondance. Vespa a également une structure de classement beaucoup plus flexible et plus simple. Un autre avantage intéressant est la possibilité d'exprimer des requêtes dans YQL, contrairement à la structure peu pratique des requêtes dans Elasticsearch. En termes de mise à l'échelle et de maintenance,puis les capacités de distribution automatique des données de Vespa se sont avérées très attrayantes pour notre équipe relativement petite. Dans l'ensemble, Vespa s'est avéré mieux prendre en charge nos cas d'utilisation et nos exigences de performances tout en étant plus facile à maintenir qu'Elasticsearch.
Elasticsearch est un moteur plus connu et nous pourrions bénéficier de l'expérience de Tinder avec lui, mais toute option nécessiterait une tonne de recherches préliminaires. Dans le même temps, Vespa sert de nombreux systèmes en production tels que Zedge , Flickr avec des milliards d'images, la plate-forme publicitaire Yahoo Gemini Ads avec plus de cent mille requêtes par seconde pour diffuser des publicités à 1 milliard d'utilisateurs actifs par mois. Cela nous a donné la confiance d'être une option éprouvée au combat, efficace et fiable - en fait, Vespa était même avant Elasticsearch.
En outre, les développeurs de Vespa se sont révélés très sociables et serviables. Vespa a été conçu à l'origine pour la publicité et le contenu. Pour autant que nous le sachions, il n'a pas encore été utilisé sur les sites de rencontres. Il était difficile d'intégrer le moteur au début car nous avions un cas d'utilisation unique, mais l'équipe Vespa s'est avérée très réactive et a rapidement optimisé le système pour nous aider à faire face à plusieurs problèmes qui se posaient.
Comment fonctionne Vespa et à quoi ressemble la recherche dans OkCupid
Avant de plonger dans notre exemple de Vespa, voici un bref aperçu de son fonctionnement. Vespa est un ensemble de nombreux services, mais chaque conteneur Docker peut être configuré pour être un hôte admin / config, un hôte de conteneur Java sans état et / ou un hôte de contenu C ++ avec état. Le package d'application avec configuration, composants, modèle ML, etc. peut être déployé via l'API Statedans un cluster de configuration qui gère l'application des modifications au conteneur et au cluster de contenu. Les demandes de flux et autres requêtes passent par un conteneur Java sans état (qui permet la personnalisation du traitement) via HTTP avant que les mises à jour de flux n'arrivent dans le cluster de contenu ou que les requêtes ne soient dirigées vers la couche de contenu, où l'exécution des requêtes distribuées se produit. Pour la plupart, le déploiement d'un nouveau package d'application ne prend que quelques secondes et Vespa traite ces modifications en temps réel dans le conteneur et le cluster de contenu, vous devez donc rarement redémarrer quoi que ce soit.
À quoi ressemble la recherche?
Les documents du cluster Vespa contiennent une variété d'attributs spécifiques à l'utilisateur. La définition de schéma définit les champs de type de document ainsi que les profils de classement qui contiennent l'ensemble des expressions de classement applicables. Supposons que nous ayons une définition de schéma qui représente un utilisateur comme celui-ci:
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
}
}
}
La notation
indexing: attribute
indique que ces champs doivent être stockés en mémoire pour fournir les meilleures performances de lecture et d'écriture pour ces champs.
Disons que nous avons rempli le cluster avec ces documents personnalisés. Nous pourrions ensuite filtrer et classer sur l'un des champs ci-dessus. Par exemple, faire une demande POST au moteur de recherche par défaut
http://localhost:8080/search/
pour trouver des utilisateurs autres que notre propre utilisateur 777
, dans un rayon de 50 miles de notre emplacement, qui sont en ligne depuis l'horodatage 1592486978
, classés par dernière activité et en conservant les deux meilleurs candidats. Sélectionnons également les fonctionnalités de synthèse pour voir la contribution de chaque expression de classement dans notre profil de classement:
{
"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"
}
}
Nous pourrions obtenir un résultat comme celui-ci:
{
"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
}
}
}
]
}
}
Après filtrage par correspondance du classement des résultats, expression calculée de la première phase (première phase) pour le classement des résultats. La pertinence renvoyée (pertinence) est le score global résultant de l'exécution de toutes les fonctions de classement de la première phase dans le profil de classement (rank-profile) que nous avons spécifié dans notre requête, c'est-à-dire
ranking.profile
myRankProfile
. ranking.features
Nous définissons query(lastOnlineWeight)
50 dans la liste , qui est ensuite référencée par la seule expression de classement que nous utilisons lastOnlineScore
. Il utilise une fonction de classement intégrée freshness
, qui est un nombre proche de 1 si l'horodatage de l'attribut est plus récent que l'horodatage actuel. Tant que tout va bien, il n'y a rien de compliqué ici.
Contrairement au contenu statique, ce contenu peut influencer s'il est montré à l'utilisateur ou non. Par exemple, ils pourraient vous aimer! Nous pourrions indexer un champ pondéré
likedUserSet
pour chaque document utilisateur contenant comme clés les identifiants des utilisateurs qu'ils aimaient et comme valeurs l'horodatage du moment où cela s'est produit. Ensuite, il serait facile de filtrer ceux qui vous ont aimé (par exemple, ajouter une expression likedUserSet contains \”777\”
dans YQL), mais comment inclure ces informations lors du classement? Comment augmenter le togr d'un utilisateur qui a aimé notre personne dans les résultats?
Dans les résultats précédents, l'expression de classement
incomingLikeScore
était de 0 pour ces deux appels. L'utilisateur a 6888497210242094612
réellement aimé l'utilisateur777
mais il n'est actuellement pas disponible dans les classements même si on l'avait mis "query(incomingLikeWeight)": 50
. Nous pouvons utiliser le rang fonction dans YQL (le premier et seul le premier argument de la fonction rank()
détermine si le document est un match, mais tous les arguments sont utilisés pour calculer le score de classement), puis utiliser dotProduct dans notre YQL expression classement pour stocker et récupérer les scores bruts (dans ce cas horodatages lorsque l'utilisateur nous a aimés), par exemple, de cette manière:
{
"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
}
}
}
]
}
}
Maintenant, l'utilisateur est
68888497210242094612
élevé au sommet, car il a aimé notre utilisateur et cela incomingLikeScore
a tout son sens. Bien sûr, nous avons en fait un horodatage de quand il nous a aimés afin que nous puissions l'utiliser dans des expressions plus complexes, mais pour l'instant, nous allons laisser les choses simples.
Cela démontre les mécanismes de filtrage et de classement des résultats à l'aide d'un système de classement. Le cadre de classement offre un moyen flexible d'appliquer des expressions (qui sont pour la plupart simplement mathématiques) aux correspondances pendant une requête.
Configuration du middleware en Java
Et si nous voulions prendre une route différente et intégrer implicitement cette expression dotProduct à chaque requête? C'est là qu'intervient la couche de conteneur Java personnalisée - nous pouvons écrire un composant Searcher personnalisé . Cela vous permet de traiter des paramètres arbitraires, de réécrire la requête et de traiter les résultats d'une manière spécifique. Voici un exemple à 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)
}
}
Ensuite, dans notre fichier services.xml , nous pouvons configurer ce composant comme suit:
...
<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>
...
Ensuite, nous créons et déployons simplement le package d'application et faisons une demande au gestionnaire personnalisé
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"
}
}
On obtient les mêmes résultats qu'avant! Notez que dans le code Kotlin, nous avons ajouté un suivi pour afficher la vue YQL après la modification, donc si elle est définie
tracelevel=2
dans les paramètres d'URL, la réponse sera également affichée:
...
{
"message": "YQL after is: select userId, summaryfeatures from user where ((rank(lastOnline > 1592486978, dotProduct(likedUserSet, {\"777\": 1})) AND !(userId contains \"777\") limit 2;"
},
...
Le conteneur middleware Java est un outil puissant pour ajouter une logique de traitement personnalisée via Searcher ou la génération native de résultats à l'aide de Renderer . Nous personnalisons nos composants Searcherpour gérer des cas comme ceux ci-dessus et d'autres aspects que nous voulons rendre implicites dans nos recherches. Par exemple, l'un des concepts de produit que nous prenons en charge est l'idée de "réciprocité". Vous pouvez rechercher des utilisateurs avec des critères spécifiques (tels que la tranche d'âge et la distance), mais vous devez également répondre aux critères de recherche des candidats. Pour prendre en charge cela dans notre composant Searcher, nous pourrions récupérer le document de l'utilisateur qui cherche à fournir certains de ses attributs dans une requête fourchue ultérieure pour le filtrage et le classement. Le cadre de classement et le middleware personnalisé offrent ensemble un moyen flexible de prendre en charge plusieurs cas d'utilisation. Nous n'avons couvert que quelques aspects dans ces exemples, mais ici vous pouvez trouver une documentation détaillée.
Comment nous avons construit un cluster Vespa et l'avons mis en production
Au printemps 2019, nous avons commencé à planifier un nouveau système. Pendant ce temps, nous avons également contacté l'équipe Vespa et nous avons régulièrement consulté sur nos cas d'utilisation. Notre équipe opérationnelle a évalué et construit la configuration initiale du cluster, et l'équipe backend a commencé à documenter, concevoir et prototyper divers cas d'utilisation de Vespa.
Les premières étapes du prototypage
Les systèmes backend OkCupid sont écrits en Golang et C ++. Afin d'écrire des composants logiques Vespa personnalisés, ainsi que de fournir des taux d'alimentation élevés à l'aide de l' API client de flux HTTP Java Vespa , nous avons dû nous familiariser un peu avec l'environnement JVM - nous avons fini par utiliser Kotlin lors de la configuration des composants Vespa et dans nos pipelines d'alimentation.
Il a fallu plusieurs années pour porter la logique de l'application et dévoiler les fonctions de Vespa, en consultant l'équipe Vespa si nécessaire. La plupart de la logique système du moteur de correspondance est écrite en C ++, nous avons donc également ajouté une logique pour traduire notre filtre actuel et le modèle de données de tri en requêtes YQL équivalentes que nous émettons au cluster Vespa via REST. Dès le début, nous avons également pris soin de créer un bon pipeline pour repeupler le cluster avec une base d'utilisateurs complète de documents; Le prototypage doit impliquer de nombreuses modifications pour déterminer les types de champs corrects à utiliser et nécessite par inadvertance de soumettre à nouveau le flux de documents.
Suivi et tests de résistance
Lorsque nous avons créé le cluster de recherche Vespa, nous devions nous assurer de deux choses: qu'il puisse gérer le volume attendu de requêtes de recherche et d'enregistrements, et que les recommandations émises par le système soient de qualité comparable à celle du système d'appariement existant.
Avant les tests de charge, nous avons ajouté des métriques Prometheus partout. Vespa-exporter fournit des tonnes de statistiques, et Vespa lui-même fournit également un petit ensemble de mesures supplémentaires . Sur cette base, nous avons créé divers tableaux de bord Grafana pour les requêtes par seconde, la latence, l'utilisation des ressources par les processus Vespa, etc. Nous avons également exécuté vespa-fbench pour tester les performances des requêtes. Avec l'aide des développeurs Vespa, nous avons déterminé qu'en raison de la relativele coût des demandes statiques, notre mise en page groupée prête à l'emploi fournira des résultats plus rapides. Dans une mise en page plate, l'ajout de nœuds supplémentaires ne réduit essentiellement que le coût d'une requête dynamique (c'est-à-dire la partie de la requête qui dépend du nombre de documents indexés). Une mise en page groupée signifie que chaque groupe de sites configuré contiendra un ensemble complet de documents, et qu'un groupe peut donc répondre à la demande. En raison du coût élevé des demandes statiques, tout en conservant le même nombre de nœuds, nous avons considérablement augmenté le débit, en augmentant le nombre d'un groupe plat à trois. Enfin, nous avons également testé en temps réel le «trafic fantôme» non signalé, lorsque nous avons pris confiance dans la fiabilité des benchmarks statiques.
Optimiser les performances
Les performances de paiement ont été l'un des plus grands obstacles auxquels nous avons été confrontés dès le début. Au tout début, nous avons eu des problèmes de traitement des mises à jour même à 1000 QPS (requêtes par seconde). Nous avons largement utilisé les champs d'ensemble pondérés, mais ils n'étaient pas efficaces au début. Heureusement, les développeurs de Vespa n'ont pas tardé à aider à résoudre ces problèmes, ainsi que d'autres liés à la diffusion des données. Plus tard, ils ont également ajouté une documentation complète sur le dimensionnement des flux , que nous utilisons dans une certaine mesure: les champs entiers dans de grands ensembles pondérés, lorsque cela est possible, permettent le traitement par lots en définissant
visibility-delay
en utilisant plusieurs mises à jour conditionnelles et en s'appuyant sur des champs d'attributs (c'est-à-dire en mémoire), ainsi qu'en réduisant le nombre de paquets aller-retour des clients en compactant et en fusionnant les opérations dans nos pipelines fmdov. Désormais, les pipelines gèrent tranquillement 3000 QPS à l'état stable, et notre humble cluster traite les mises à jour de 11K QPS lorsqu'un tel pic se produit pour une raison quelconque.
Qualité des recommandations
Après avoir été convaincu que le cluster peut gérer la charge, il a fallu vérifier que la qualité des recommandations n'est pas pire que dans le système existant. Tout écart mineur dans la mise en œuvre du classement a un impact énorme sur la qualité globale des recommandations et sur l'écosystème global dans son ensemble. Nous avons appliqué un système expérimentalVespa dans certains groupes de test, tandis que le groupe témoin a continué à utiliser le système existant. Plusieurs mesures commerciales ont ensuite été analysées, répétant et documentant les problèmes jusqu'à ce que le groupe Vespa fonctionne aussi bien, sinon mieux, que le groupe témoin. Une fois que nous étions confiants dans les résultats Vespa, il était facile de transmettre les demandes de correspondance au cluster Vespa. Nous avons pu lancer tout le trafic de recherche dans le cluster Vespa sans accroc!
Diagramme système
Sous une forme simplifiée, le schéma d'architecture final du nouveau système ressemble à ceci:
Comment Vespa fonctionne maintenant et quelle est la suite
Comparons l'état du chercheur de paires Vespa avec le système précédent:
- Mises à jour du schéma
- Avant: une semaine avec des centaines de nouvelles lignes de code, un déploiement soigneusement coordonné avec plusieurs sous-systèmes
- :
- Avant: une semaine avec des centaines de nouvelles lignes de code, un déploiement soigneusement coordonné avec plusieurs sous-systèmes
- /
- :
- : . , !
- :
-
- : ,
- : , Vespa . -
- : ,
Dans l'ensemble, l'aspect conception et maintenance du cluster Vespa a contribué au développement de tous les produits OkCupid. Fin janvier 2020, nous avons lancé notre cluster Vespa en production et il sert toutes les recommandations dans la recherche de paires. Nous avons également ajouté des dizaines de nouveaux champs, expressions de classement et cas d'utilisation avec prise en charge de toutes les nouvelles fonctionnalités de cette année, telles que Stacks . Et contrairement à notre précédent système de matchmaking, nous utilisons désormais des modèles d'apprentissage automatique en temps réel au moment des requêtes.
Et après?
Pour nous, l'un des principaux avantages de Vespa est la prise en charge directe du classement à l'aide de tenseurs et l'intégration avec des modèles entraînés à l'aide de frameworks tels que TensorFlow . C'est l'une des principales fonctionnalités que nous développerons dans les mois à venir. Nous utilisons déjà des tenseurs pour certains cas d'utilisation et envisagerons bientôt d'intégrer différents modèles d'apprentissage automatique qui, nous l'espérons, permettront de mieux prédire les résultats et les correspondances pour nos utilisateurs.
En outre, Vespa a récemment annoncé la prise en charge des index multidimensionnels des plus proches voisins, qui sont entièrement en temps réel, consultables simultanément et mis à jour dynamiquement. Nous sommes très intéressés par l'exploration d'autres cas d'utilisation pour la recherche d' index des plus proches voisins en temps réel.
OkCupid et Vespa. Aller!
De nombreuses personnes ont entendu ou travaillé avec Elasticsearch, mais il n'y a pas une communauté aussi grande autour de Vespa. Nous pensons que de nombreuses autres applications Elasticsearch fonctionneraient mieux sur Vespa. C'est génial pour OkCupid, et nous sommes heureux de l'avoir changé. Cette nouvelle architecture nous a permis d'évoluer et de développer de nouvelles fonctionnalités beaucoup plus rapidement. Nous sommes une entreprise relativement petite, il est donc bon de ne pas trop s'inquiéter de la complexité du service. Nous sommes désormais bien mieux préparés à étendre notre moteur de recherche. Sans Vespa, nous n'aurions certainement pas pu faire les progrès que nous avons réalisés au cours de l'année écoulée. Pour plus d'informations sur les capacités techniques de Vespa, assurez-vous de consulter les directives Vespa AI in Ecommerce de @jobergum .
Nous avons fait le premier pas et avons aimé les développeurs Vespa. Ils nous ont renvoyé un message et cela s'est avéré être une coïncidence! Nous n'aurions pas pu faire cela sans l'aide de l'équipe Vespa. Un merci spécial à @jobergum et @geirst pour leurs recommandations sur le classement et la gestion des requêtes, et @kkraune et @vekterli pour leur soutien. Le niveau de soutien et d'effort que l'équipe Vespa nous a apporté a été vraiment incroyable - de la compréhension approfondie de notre cas d'utilisation au diagnostic des problèmes de performances et à l'amélioration immédiate du moteur Vespa. Le camarade @vekterli s'est même envolé dans notre bureau de New York et a travaillé directement avec nous pendant une semaine pour aider à intégrer le moteur. Un grand merci à l'équipe Vespa!
En conclusion, nous n'avons abordé que quelques aspects de l'utilisation de Vespa, mais rien de tout cela n'aurait été possible sans l'énorme travail de nos équipes backend et opérationnelles au cours de l'année écoulée. Nous avons rencontré de nombreux défis uniques pour combler le fossé entre les systèmes existants et la pile technologique plus moderne, mais ce sont des sujets pour d'autres articles.