Vespa es mejor que Elasticsearch para combinar a millones de hombres y mujeres





Una parte integral del sitio de citas OkCupid es la recomendación de socios potenciales. Se basan en la superposición de muchas preferencias que usted y sus socios potenciales han indicado. Como puede imaginar, hay muchas formas de optimizar esta tarea.



Sin embargo, tus preferencias no son el único factor que influye en quién te recomendamos como socio potencial (o te recomendamos a ti mismo como socio potencial para otros). Si simplemente tuviéramos que mostrar todos los usuarios que coinciden con sus criterios, sin ningún ranking, entonces la lista no sería óptima en absoluto. Por ejemplo, si ignora la actividad reciente del usuario, puede pasar mucho más tiempo hablando con una persona que no visita el sitio. Además de las preferencias que especifique, utilizamos numerosos algoritmos y factores para recomendarle las personas que creemos que debería ver.



Debemos ofrecer los mejores resultados posibles y una lista casi interminable de recomendaciones. En otras aplicaciones, donde el contenido cambia con menos frecuencia, puede hacerlo actualizando periódicamente las recomendaciones. Por ejemplo, cuando usa la función "Discover Weekly" de Spotify, disfruta de un conjunto de pistas recomendadas, este conjunto no cambia hasta la próxima semana. En OkCupid, los usuarios ven sus recomendaciones sin cesar en tiempo real. El “contenido” recomendado es de naturaleza muy dinámica (por ejemplo, un usuario puede cambiar sus preferencias, datos de perfil, ubicación, desactivar en cualquier momento, etc.). El usuario puede cambiar quién y cómo puede recomendarlo, por lo que queremos asegurarnos de que las posibles coincidencias sean las mejores en un momento dado.



Para aprovechar los diferentes algoritmos de clasificación y hacer recomendaciones en tiempo real, debe utilizar un motor de búsqueda que se actualice constantemente con los datos de los usuarios y brinde la capacidad de filtrar y clasificar a los candidatos potenciales.



¿Cuáles son los problemas con el sistema de búsqueda de coincidencias existente?



OkCupid ha estado utilizando su propio motor de búsqueda interno durante años. No entraremos en detalles, pero en un alto nivel de abstracción, es un marco de reducción de mapas sobre fragmentos de espacio de usuario, donde cada fragmento contiene algunos de los datos de usuario relevantes en la memoria, que se utiliza para habilitar varios filtros y clasificaciones sobre la marcha. Los términos de búsqueda divergen en todos los fragmentos y, en última instancia, los resultados se combinan para devolver los k candidatos principales. Este sistema de emparejamiento que escribimos funcionó bien, entonces, ¿por qué decidimos cambiarlo ahora?



Sabíamos que necesitábamos actualizar el sistema para respaldar varios proyectos basados ​​en recomendaciones en los próximos años. Sabíamos que nuestro equipo iba a crecer, al igual que la cantidad de proyectos. Uno de los mayores desafíos fue actualizar el esquema. Por ejemplo, agregar una nueva pieza de datos de usuario (por ejemplo, etiquetas de género en las preferencias) requería cientos o miles de líneas de código en las plantillas, y la implementación requería una coordinación cuidadosa para garantizar que todas las partes del sistema se implementaran en el orden correcto. Simplemente intentar agregar una nueva forma de filtrar un conjunto de datos personalizado o clasificar los resultados tomó medio día de tiempo de ingeniería. Tuvo que implementar manualmente cada segmento en producción y monitorear posibles problemas. Más importante aún, se ha vuelto difícil administrar y escalar el sistema,porque los fragmentos y las réplicas se distribuyeron manualmente en una flota de máquinas que no tenían ningún software instalado.



A principios de 2019, la carga en el sistema de emparejamiento aumentó, por lo que agregamos otro conjunto de réplicas colocando manualmente instancias de servicio en varias máquinas. El trabajo llevó muchas semanas en el backend y para los devops. Durante este tiempo, también comenzamos a notar cuellos de botella en el rendimiento en el descubrimiento de servicios integrados, cola de mensajes, etc. Si bien estos componentes funcionaban bien anteriormente, habíamos llegado a un punto en el que comenzamos a cuestionar la escalabilidad de estos sistemas. Nuestra tarea consistía en trasladar la mayor parte de nuestra carga de trabajo a la nube. Portar este sistema de emparejamiento es una tarea tediosa en sí misma, pero también involucra a otros subsistemas.



Hoy en OkCupid, muchos de estos subsistemas cuentan con opciones de OSS más robustas y compatibles con la nube, y el equipo ha implementado varias tecnologías con gran éxito durante los últimos dos años. No entraremos en estos proyectos aquí, sino que nos centraremos en los pasos que tomamos para abordar los problemas anteriores, pasando a un motor de búsqueda más escalable y amigable para los desarrolladores para nuestras recomendaciones: Vespa .



¡Eso es una coincidencia! Por qué OkCupid se hizo amigo de Vespa



Históricamente, nuestro equipo ha sido pequeño. Sabíamos desde el principio que elegir el núcleo de un motor de búsqueda sería extremadamente difícil, por lo que analizamos las opciones de código abierto que nos funcionaron. Los dos principales contendientes fueron Elasticsearch y Vespa.



Elasticsearch



Es una tecnología popular con una gran comunidad, buena documentación y soporte. Hay toneladas de funciones e incluso se usa en Tinder . Se pueden agregar nuevos campos de esquema usando el mapeo PUT, las consultas se pueden realizar usando llamadas REST estructuradas, hay cierto soporte para la clasificación por tiempo de consulta, la capacidad de escribir complementos personalizados, etc. Cuando se trata de escalado y mantenimiento, solo necesita definir el número de fragmentos y el propio sistema maneja la distribución de réplicas. El escalado requiere reconstruir otro índice con más fragmentos.



Una de las principales razones por las que abandonamos Elasticsearch fue la falta de verdaderas actualizaciones parciales en la memoria. Esto es muy importante para nuestro caso de uso, porque los documentos que estamos a punto de indexar deben actualizarse muy a menudo debido a los me gusta, mensajes, etc. Estos documentos son de naturaleza muy dinámica, en comparación con el contenido como anuncios o imágenes, que son en su mayoría objetos estáticos con atributos constantes. Por lo tanto, los ciclos de lectura y escritura ineficaces en las actualizaciones fueron un problema de rendimiento importante para nosotros.



Vespa



El código fuente se abrió hace solo unos años. Los desarrolladores anunciaron soporte para almacenar, buscar, clasificar y organizar Big Data en tiempo real. Características que admite Vespa:



  • ( , 40-50 . )

  • ,

  • (, TensorFlow)

  • YQL (Yahoo Query Language) REST

  • Java-


Cuando se trata de escalado y mantenimiento, ya no piensa en fragmentos  : configura el diseño de sus nodos de contenido y Vespa maneja automáticamente cómo fragmentar documentos, replicar y distribuir datos. Además, los datos se restauran y se redistribuyen automáticamente desde las réplicas cada vez que agrega o elimina nodos. Escalar simplemente significa actualizar la configuración para agregar nodos y permite que Vespa redistribuya automáticamente estos datos en tiempo real.



En general, Vespa parecía ser la mejor opción para nuestros casos de uso. OkCupid incluye mucha información diferente sobre los usuarios para ayudarlos a encontrar la mejor combinación; en términos de solo filtros y tipos, ¡hay más de cien parámetros! Siempre agregaremos filtros y ordenaciones, por lo que mantener este flujo de trabajo es muy importante. En términos de entradas y consultas, Vespa es más similar a nuestro sistema existente; es decir, nuestro sistema también requería procesar actualizaciones parciales rápidas en la memoria y procesamiento en tiempo real durante una solicitud de coincidencia. Vespa también tiene una estructura de clasificación mucho más flexible y simple. Otra buena ventaja es la capacidad de expresar consultas en YQL, en contraste con la estructura inconveniente para consultas en Elasticsearch. En términos de escalado y mantenimiento,Entonces, las capacidades de distribución automática de datos de Vespa resultaron muy atractivas para nuestro equipo relativamente pequeño. En general, se descubrió que Vespa respalda mejor nuestros casos de uso y requisitos de rendimiento, al tiempo que es más fácil de mantener que Elasticsearch.



Elasticsearch es un motor más conocido y podríamos beneficiarnos de la experiencia de Tinder con él, pero cualquier opción requeriría mucha investigación preliminar. Al mismo tiempo, Vespa sirve a muchos sistemas en producción como Zedge , Flickr con miles de millones de imágenes, la plataforma publicitaria Yahoo Gemini Ads con más de cien mil solicitudes por segundo para servir anuncios a mil millones de usuarios activos mensuales. Esto nos dio la confianza para ser una opción probada en batalla, eficiente y confiable; de ​​hecho, Vespa fue incluso antes que Elasticsearch.



Además, los desarrolladores de Vespa han demostrado ser muy sociables y serviciales. Vespa se construyó originalmente para publicidad y contenido. Hasta donde sabemos, todavía no se ha utilizado en sitios de citas. Fue difícil integrar el motor al principio porque teníamos un caso de uso único, pero el equipo de Vespa demostró ser muy receptivo y optimizó rápidamente el sistema para ayudarnos a lidiar con varios problemas que surgieron.



Cómo funciona Vespa y qué aspecto tiene la búsqueda en OkCupid







Antes de sumergirnos en nuestro ejemplo de Vespa, aquí hay una descripción general rápida de cómo funciona. Vespa es una colección de numerosos servicios, pero cada contenedor Docker se puede configurar para que sea un host de administración / configuración, un host contenedor Java sin estado y / o un host de contenido C ++ con estado. El paquete de aplicación con configuración, componentes, modelo ML, etc. se puede implementar a través de la API de estadoen un clúster de configuración que maneja la aplicación de cambios al contenedor y al clúster de contenido. Las solicitudes de feeds y otras solicitudes pasan por un contenedor Java sin estado (que permite la personalización del procesamiento) a través de HTTP antes de que las actualizaciones de feeds lleguen al clúster de contenido o las solicitudes se bifurquen a la capa de contenido, donde se produce la ejecución de la solicitud distribuida. En su mayor parte, implementar un nuevo paquete de aplicaciones solo toma unos segundos, y Vespa procesa estos cambios en tiempo real en el contenedor y el grupo de contenido, por lo que rara vez tiene que reiniciar nada.



¿Qué aspecto tiene la búsqueda?



Los documentos del grupo Vespa contienen una variedad de atributos específicos del usuario. La definición del esquema define los campos de tipo de documento, así como los perfiles de clasificación que contienen el conjunto de expresiones de clasificación aplicables. Supongamos que tenemos una definición de esquema que representa a un usuario como este:



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 notación indexing: attributeindica que estos campos deben almacenarse en la memoria para proporcionar el mejor rendimiento de lectura y escritura para estos campos.



Digamos que llenamos el clúster con estos documentos personalizados. Luego, podríamos filtrar y clasificar en cualquiera de los campos anteriores. Por ejemplo, hacer una solicitud POST al motor de búsqueda predeterminado http://localhost:8080/search/para encontrar usuarios que no sean nuestro propio usuario 777, dentro de 50 millas de nuestra ubicación, que hayan estado en línea desde la marca de tiempo 1592486978, clasificados por última actividad y manteniendo los dos mejores candidatos. Seleccionemos también las características de resumen para ver la contribución de cada expresión de clasificación en nuestro perfil de clasificación:



{
    "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"
    }
}


Podríamos obtener un resultado como este:



{
    "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
                    }
                }
            }
        ]
    }
}


Después de filtrar por la clasificación de resultados coincidentes, la expresión calculada de la primera fase (primera fase) para clasificar los resultados. La relevancia devuelta es la puntuación general como resultado de realizar todas las funciones de clasificación de la primera fase en el perfil de clasificación que especificamos en nuestra consulta, es decir ranking.profile myRankProfile. ranking.featuresDefinimos query(lastOnlineWeight)50 en la lista , que luego es referenciada por la única expresión de clasificación que usamos lastOnlineScore. Utiliza una función de clasificación incorporada freshness , que es un número cercano a 1 si la marca de tiempo en el atributo es más reciente que la marca de tiempo actual. Mientras todo vaya bien, aquí no hay nada complicado.



A diferencia del contenido estático, este contenido puede influir en si se muestra al usuario o no. Por ejemplo, ¡podrías gustarles! Podríamos indexar un campo ponderado likedUserSet para cada documento de usuario que contenga como claves los ID de los usuarios que les gustaron y como valores la marca de tiempo de cuándo sucedió. Entonces sería fácil filtrar a aquellos a quienes les likedUserSet contains \”777\”gustaste (por ejemplo, agregando una expresión en YQL), pero ¿cómo incluir esta información durante la clasificación? ¿Cómo incrementar el togr de un usuario al que le gustó nuestra persona en los resultados?



En resultados anteriores, la expresión de clasificación incomingLikeScoreera 0 para ambos hits. Al usuario 6888497210242094612realmente le gustó el usuario777pero actualmente no está disponible en el ranking incluso si lo hubiéramos puesto "query(incomingLikeWeight)": 50. Podemos usar la función de rango en YQL (el primer y único argumento de la función rank()determina si el documento es una coincidencia, pero todos los argumentos se usan para calcular la puntuación de clasificación) y luego usar dotProduct en nuestra expresión de clasificación de YQL para almacenar y recuperar las puntuaciones sin procesar (en este caso marcas de tiempo cuando le gustamos al usuario), por ejemplo, de esta manera:



{
    "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
                    }
                }
            }
        ]
    }
}


Ahora el usuario se 68888497210242094612eleva a la cima, ya que le gustó nuestro usuario y incomingLikeScoretiene pleno significado. Por supuesto, en realidad tenemos una marca de tiempo de cuándo le gustamos para que podamos usarla en expresiones más complejas, pero por ahora lo dejaremos simple.



Esto demuestra la mecánica de filtrar y clasificar los resultados mediante un sistema de clasificación. El marco de clasificación proporciona una forma flexible de aplicar expresiones (que en su mayoría son solo matemáticas) a las coincidencias durante una consulta.



Configuración de middleware en Java



¿Qué pasaría si quisiéramos tomar una ruta diferente y hacer que esta expresión dotProduct sea parte implícita de cada solicitud? Aquí es donde entra la capa de contenedor Java personalizada: podemos escribir un componente de búsqueda personalizado . Esto le permite procesar parámetros arbitrarios, reescribir la consulta y procesar los resultados de una manera específica. Aquí hay un ejemplo en 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)
    }
}


Luego, en nuestro archivo services.xml , podemos configurar este componente de la siguiente manera:



...       
         <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>
...


Luego, simplemente creamos e implementamos el paquete de la aplicación y hacemos una solicitud al controlador personalizado 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"
    }
}


¡Obtenemos los mismos resultados que antes! Tenga en cuenta que en el código de Kotlin, agregamos un rastreo para generar la vista YQL después del cambio, por lo que si se establece tracelevel=2en los parámetros de URL, la respuesta también se mostrará:



...
                    {
                        "message": "YQL after is: select userId, summaryfeatures from user where ((rank(lastOnline > 1592486978, dotProduct(likedUserSet, {\"777\": 1})) AND !(userId contains \"777\") limit 2;"
                    },
...


El contenedor de middleware de Java es una herramienta poderosa para agregar lógica de procesamiento personalizada a través del buscador o la generación nativa de resultados usando Renderer . Personalizamos los componentes de nuestro buscadormanejar casos como los anteriores y otros aspectos que queramos dejar implícitos en nuestras búsquedas. Por ejemplo, uno de los conceptos de producto que apoyamos es la idea de "reciprocidad": puede buscar usuarios con criterios específicos (como rango de edad y distancia), pero también debe cumplir con los criterios de búsqueda de candidatos. Para respaldar esto en nuestro componente Buscador, podríamos recuperar el documento del usuario que está buscando para proporcionar algunos de sus atributos en una consulta bifurcada posterior para filtrar y clasificar. El marco de clasificación y el middleware personalizado juntos proporcionan una forma flexible de admitir múltiples casos de uso. Solo hemos cubierto algunos aspectos en estos ejemplos, pero aquí puede encontrar documentación detallada.



Cómo construimos un clúster Vespa y lo pusimos en producción



En la primavera de 2019, comenzamos a planificar un nuevo sistema. Durante este tiempo, también nos pusimos en contacto con el equipo de Vespa y lo consultamos regularmente sobre nuestros casos de uso. Nuestro equipo de operaciones evaluó y construyó la configuración inicial del clúster, y el equipo de backend comenzó a documentar, diseñar y crear prototipos de varios casos de uso de Vespa.



Las primeras etapas de la creación de prototipos



Los sistemas backend de OkCupid están escritos en Golang y C ++. Para escribir componentes lógicos de Vespa personalizados, así como proporcionar altas velocidades de alimentación utilizando la API del cliente de alimentación HTTP de Java Vespa , tuvimos que familiarizarnos un poco con el entorno JVM; terminamos usando Kotlin al configurar los componentes de Vespa y en nuestras tuberías de alimentación.



Se necesitaron varios años para adaptar la lógica de la aplicación y revelar las funciones de Vespa, consultando con el equipo de Vespa según fuera necesario. La mayor parte de la lógica del sistema del motor de coincidencia está escrita en C ++, por lo que también agregamos lógica para traducir nuestro filtro actual y ordenar el modelo de datos en consultas YQL equivalentes que enviamos al clúster de Vespa a través de REST. Al principio, también nos ocupamos de crear una buena canalización para repoblar el clúster con una base de usuarios completa de documentos; La creación de prototipos debe implicar muchos cambios para determinar los tipos de campo correctos que se deben usar y, sin darse cuenta, requiere volver a enviar el documento.



Monitoreo y pruebas de estrés



Cuando creamos el grupo de búsqueda Vespa, tuvimos que asegurarnos de dos cosas: que pueda manejar el volumen esperado de consultas y registros de búsqueda, y que las recomendaciones que brinda el sistema sean comparables en calidad al sistema de emparejamiento existente.



Antes de las pruebas de carga, agregamos métricas de Prometheus en todas partes. Vespa-exporter proporciona toneladas de estadísticas, y la propia Vespa también proporciona un pequeño conjunto de métricas adicionales . En base a esto, creamos varios paneles de Grafana para solicitudes por segundo, latencia, utilización de recursos por procesos de Vespa, etc. También ejecutamos vespa-fbench para probar el rendimiento de las consultas. Con la ayuda de los desarrolladores de Vespa, hemos determinado que debido a la relativamente altaEl costo de las solicitudes estáticas, nuestro diseño agrupado listo para usar proporcionará resultados más rápidos. En un diseño plano, agregar más nodos básicamente solo reduce el costo de una consulta dinámica (es decir, la parte de la consulta que depende de la cantidad de documentos indexados). Un diseño agrupado significa que cada grupo de sitio configurado contendrá un conjunto completo de documentos y, por lo tanto, un grupo puede atender la solicitud. Debido al alto costo de las solicitudes estáticas, mientras manteníamos la misma cantidad de nodos, aumentamos significativamente el rendimiento, aumentando la cantidad de un grupo plano a tres. Finalmente, también probamos el "tráfico en la sombra" no reportado en tiempo real cuando confiamos en la confiabilidad de los puntos de referencia estáticos.



Optimización del rendimiento



El rendimiento de la caja fue uno de los mayores obstáculos que enfrentamos al principio. Al principio, tuvimos problemas para procesar actualizaciones incluso a 1000 QPS (solicitudes por segundo). Usamos extensivamente los campos de conjuntos ponderados, pero al principio no fueron efectivos. Afortunadamente, los desarrolladores de Vespa se apresuraron a ayudar a resolver estos problemas, así como otros relacionados con la difusión de datos. Más tarde, también agregaron una extensa documentación sobre el tamaño del alimento , que usamos hasta cierto punto: los campos de números enteros en conjuntos grandes ponderados, cuando es posible, permiten el procesamiento por lotes mediante la configuraciónvisibility-delaymediante el uso de múltiples actualizaciones condicionales y confiando en los campos de atributos (es decir, en la memoria), así como también reduciendo la cantidad de paquetes de ida y vuelta de los clientes compactando y fusionando operaciones en nuestras canalizaciones fmdov. Ahora las canalizaciones están manejando silenciosamente 3000 QPS en estado estable, y nuestro humilde clúster está procesando actualizaciones de 11K QPS cuando ocurre tal pico por alguna razón.



Calidad de las recomendaciones



Después de estar convencidos de que el clúster puede manejar la carga, fue necesario verificar que la calidad de las recomendaciones no sea peor que en el sistema existente. Cualquier desviación menor en la implementación de la clasificación tiene un gran impacto en la calidad general de las recomendaciones y en el ecosistema en general. Aplicamos un sistema experimentalVespa en algunos grupos de prueba, mientras que el grupo de control continuó usando el sistema existente. Luego se analizaron varias métricas comerciales, repitiendo y documentando los problemas hasta que el grupo Vespa se desempeñó tan bien, si no mejor, que el grupo de control. Una vez que confiamos en los resultados de Vespa, fue fácil enviar las solicitudes de coincidencia al grupo de Vespa. ¡Pudimos lanzar todo el tráfico de búsqueda al grupo de Vespa sin ningún problema!



Sistema de diagrama



En una forma simplificada, el diagrama de arquitectura final del nuevo sistema se ve así:







Cómo funciona Vespa ahora y qué sigue



Comparemos el estado del buscador de pares de Vespa con el sistema anterior:



  • Actualizaciones de esquemas

    • Antes: una semana con cientos de nuevas líneas de código, implementación cuidadosamente coordinada con múltiples subsistemas

    • :
  • /

    • :

    • : . , !


    • : ,

    • : , Vespa . -


En general, el aspecto de diseño y mantenimiento del grupo Vespa ha ayudado al desarrollo de todos los productos OkCupid. A finales de enero de 2020, lanzamos nuestro clúster Vespa a producción y cumple todas las recomendaciones en la búsqueda de parejas. También hemos agregado docenas de nuevos campos, expresiones de clasificación y casos de uso con soporte para todas las funciones nuevas de este año, como Stacks . Y a diferencia de nuestro sistema de emparejamiento anterior, ahora usamos modelos de aprendizaje automático en tiempo real en el momento de la consulta.



¿Que sigue?



Para nosotros, una de las principales ventajas de Vespa es el soporte directo para la clasificación usando tensores y la integración con modelos entrenados usando frameworks como TensorFlow . Esta es una de las principales características que estaremos desarrollando en los próximos meses. Ya estamos usando tensores para algunos casos de uso, y pronto buscaremos integrar diferentes modelos de aprendizaje automático que esperamos puedan predecir mejor los resultados y las coincidencias para nuestros usuarios.



Además, Vespa anunció recientemente el soporte para índices de vecinos más cercanos multidimensionales, que son completamente en tiempo real, se pueden buscar y actualizar dinámicamente simultáneamente. Estamos muy interesados ​​en explorar otros casos de uso para la búsqueda de índices de vecinos más cercanos en tiempo real.



OkCupid y Vespa. ¡Vamos!



Mucha gente ha escuchado o trabajado con Elasticsearch, pero no hay una comunidad tan grande alrededor de Vespa. Creemos que muchas otras aplicaciones de Elasticsearch funcionarían mejor en Vespa. Es genial para OkCupid y nos alegra haberlo cambiado. Esta nueva arquitectura nos permitió evolucionar y desarrollar nuevas funciones mucho más rápido. Somos una empresa relativamente pequeña, por lo que es bueno no preocuparse demasiado por la complejidad del servicio. Ahora estamos mucho mejor preparados para escalar nuestro motor de búsqueda. Sin Vespa, ciertamente no podríamos haber logrado el progreso que hicimos durante el año pasado. Para obtener más información acerca de las capacidades técnicas del Vespa, asegúrese de revisar la Vespa AI en el comercio electrónico Directrices de @jobergum .



Dimos el primer paso y nos gustaron los desarrolladores de Vespa. ¡Nos enviaron un mensaje y resultó ser una coincidencia! No podríamos haberlo hecho sin la ayuda del equipo Vespa. Un agradecimiento especial a @jobergum y @geirst por sus recomendaciones sobre clasificación y manejo de consultas, y a @kkraune y @vekterli por su apoyo. El nivel de apoyo y esfuerzo que nos ha brindado el equipo de Vespa ha sido realmente sorprendente, desde una visión profunda de nuestro caso de uso hasta el diagnóstico de problemas de rendimiento y la realización de mejoras inmediatas en el motor de Vespa. El camarada @vekterli incluso voló a nuestra oficina de Nueva York y trabajó directamente con nosotros durante una semana para ayudar a integrar el motor. ¡Muchas gracias al equipo Vespa!



En conclusión, solo hemos abordado algunos aspectos del uso de Vespa, pero nada de esto habría sido posible sin el tremendo trabajo de nuestros equipos de backend y operaciones durante el año pasado. Nos encontramos con muchos desafíos únicos para cerrar la brecha entre los sistemas existentes y la pila de tecnología más moderna, pero estos son temas para otros artículos.



All Articles