Vespa é melhor do que Elasticsearch para milhões de homens e mulheres correspondentes





Uma parte integrante do site de namoro OkCupid é a recomendação de parceiros em potencial. Eles são baseados na sobreposição de muitas preferências que você e seus parceiros em potencial indicaram. Como você pode imaginar, existem muitas maneiras de otimizar essa tarefa.



No entanto, suas preferências não são o único fator que influencia quem recomendamos você como parceiro potencial (ou recomendamos você mesmo como parceiro potencial para outros). Se estivéssemos apenas mostrando todos os usuários que correspondem aos seus critérios, sem qualquer classificação, a lista não seria a ideal de forma alguma. Por exemplo, se você ignorar a atividade recente do usuário, poderá passar muito mais tempo conversando com uma pessoa que não visita o site. Além das preferências que você especifica, usamos vários algoritmos e fatores para recomendar as pessoas que achamos que você deveria ver.



Devemos entregar os melhores resultados possíveis e uma lista quase infinita de recomendações. Em outros aplicativos, onde o conteúdo muda com menos frequência, você pode fazer isso atualizando periodicamente as recomendações. Por exemplo, ao usar o recurso “Discover Weekly” do Spotify, você desfruta de um conjunto de faixas recomendadas, este conjunto não muda até a próxima semana. No OkCupid, os usuários veem continuamente suas recomendações em tempo real. O “conteúdo” recomendado é muito dinâmico por natureza (por exemplo, um usuário pode alterar suas preferências, dados de perfil, localização, desativar a qualquer momento, etc.). O usuário pode mudar quem e como ele pode recomendá-lo, por isso queremos ter certeza de que as correspondências potenciais são as melhores em um determinado momento.



Para aproveitar as vantagens de diferentes algoritmos de classificação e fazer recomendações em tempo real, você precisa usar um mecanismo de pesquisa que seja constantemente atualizado com os dados do usuário e forneça a capacidade de filtrar e classificar os candidatos em potencial.



Quais são os problemas com o sistema de busca de correspondência existente



O OkCupid tem usado seu próprio mecanismo de busca interno há anos. Não entraremos em detalhes, mas em um alto nível de abstração, é uma estrutura de redução de mapa sobre fragmentos de espaço do usuário, onde cada fragmento contém alguns dos dados relevantes do usuário na memória, que são usados ​​ao ativar vários filtros e classificações instantaneamente. Os termos de pesquisa divergem em todos os fragmentos e, por fim, os resultados são combinados para retornar os k candidatos principais. Este sistema de emparelhamento que escrevemos funcionou bem, então por que decidimos mudá-lo agora?



Sabíamos que precisávamos atualizar o sistema para dar suporte a vários projetos baseados em recomendações nos próximos anos. Sabíamos que nossa equipe iria crescer e também o número de projetos. Um dos maiores desafios foi atualizar o esquema. Por exemplo, adicionar uma nova parte dos dados do usuário (digamos, tags de gênero nas preferências) exigia centenas ou milhares de linhas de código nos modelos, e a implantação exigia uma coordenação cuidadosa para garantir que todas as partes do sistema fossem implantadas na ordem correta. A simples tentativa de adicionar uma nova maneira de filtrar um conjunto de dados personalizado ou classificar os resultados levou meio dia de trabalho do engenheiro. Ele teve que implantar manualmente cada segmento em produção e monitorar possíveis problemas. Mais importante, tornou-se difícil gerenciar e dimensionar o sistema,porque os fragmentos e réplicas foram distribuídos manualmente em uma frota de máquinas que não tinha nenhum software instalado.



No início de 2019, a carga no sistema de emparelhamento aumentou, então adicionamos outro conjunto de réplicas colocando manualmente as instâncias de serviço em várias máquinas. O trabalho demorou muitas semanas no backend e para os devops. Durante esse tempo, também começamos a notar gargalos de desempenho na descoberta de serviços incorporados, enfileiramento de mensagens e assim por diante.Embora esses componentes funcionassem bem, chegamos a um ponto em que começamos a questionar a escalabilidade desses sistemas. Nossa tarefa era mover a maior parte de nossa carga de trabalho para a nuvem. Portar este sistema de emparelhamento é uma tarefa tediosa em si, mas também envolve outros subsistemas.



Hoje, no OkCupid, muitos desses subsistemas são servidos por opções de OSS mais robustas e amigáveis ​​à nuvem, e a equipe adotou várias tecnologias com grande sucesso nos últimos dois anos. Não entraremos nesses projetos aqui, mas sim nos concentraremos nas etapas que tomamos para resolver os problemas acima, passando para um mecanismo de pesquisa mais escalável e amigável para o desenvolvedor para nossas recomendações: Vespa .



Isso é uma coincidência! Por que o OkCupid se tornou amigo da Vespa



Historicamente, nossa equipe é pequena. Sabíamos desde o início que escolher um mecanismo de pesquisa seria extremamente difícil, então examinamos as opções de código aberto que funcionavam para nós. Os dois principais contendores eram Elasticsearch e Vespa.



Elasticsearch



É uma tecnologia popular com uma grande comunidade, boa documentação e suporte. Existem muitos recursos e até mesmo é usado no Tinder . Novos campos de esquema podem ser adicionados usando o mapeamento PUT, as consultas podem ser feitas usando chamadas REST estruturadas, há algum suporte para classificação por tempo de consulta, a capacidade de escrever plug-ins personalizados, etc. Quando se trata de dimensionamento e manutenção, você só precisa definir o número de fragmentos , e o próprio sistema lida com a distribuição de réplicas. O escalonamento requer a reconstrução de outro índice com mais fragmentos.



Um dos principais motivos pelos quais abandonamos o Elasticsearch foi a falta de atualizações parciais verdadeiras na memória. Isso é muito importante para o nosso caso de uso, porque os documentos que vamos indexar devem ser atualizados com muita frequência devido a curtidas, mensagens, etc. Esses documentos são muito dinâmicos por natureza, em comparação com conteúdo como anúncios ou imagens, que são principalmente objetos estáticos com atributos constantes. Portanto, ciclos ineficientes de leitura e gravação em atualizações eram um grande problema de desempenho para nós.



Vespa



O código-fonte foi aberto apenas alguns anos atrás. Os desenvolvedores anunciaram suporte para armazenamento, pesquisa, classificação e organização de Big Data em tempo real. Recursos compatíveis com o Vespa:



  • ( , 40-50 . )

  • ,

  • (, TensorFlow)

  • YQL (Yahoo Query Language) REST

  • Java-


Quando se trata de dimensionamento e manutenção, você não pensa mais em shards  - você configura o layout para os nós de conteúdo, e a Vespa automaticamente controla como fragmentar documentos, replicar e distribuir dados. Além disso, os dados são restaurados e redistribuídos automaticamente das réplicas sempre que você adiciona ou remove nós. Escalonar significa simplesmente atualizar a configuração para adicionar nós e permite que a Vespa redistribua automaticamente esses dados em tempo real.



No geral, a Vespa parecia ser a melhor opção para nossos casos de uso. O OkCupid inclui muitas informações diferentes sobre os usuários para ajudá-los a encontrar a melhor correspondência - em termos de apenas filtros e classificações, existem mais de cem parâmetros! Estaremos sempre adicionando filtros e classificações, por isso é muito importante manter esse fluxo de trabalho. Em termos de entradas e consultas, o Vespa é mais semelhante ao nosso sistema existente; ou seja, nosso sistema também exigia o processamento de atualizações parciais rápidas na memória e processamento em tempo real durante uma solicitação de correspondência. A Vespa também tem uma estrutura de classificação muito mais flexível e simples. Outro bom bônus é a capacidade de expressar consultas em YQL, em contraste com a estrutura inconveniente para consultas no Elasticsearch. Em termos de dimensionamento e manutenção,então, os recursos de distribuição automática de dados da Vespa eram muito atraentes para nossa equipe relativamente pequena. No geral, Vespa oferece suporte melhor aos nossos casos de uso e requisitos de desempenho, além de ser mais fácil de manter do que o Elasticsearch.



Elasticsearch é um mecanismo mais conhecido e poderíamos nos beneficiar da experiência do Tinder com ele, mas qualquer opção exigiria uma tonelada de pesquisas preliminares. Ao mesmo tempo, a Vespa atende a muitos sistemas em produção, como Zedge , Flickr com bilhões de imagens, plataforma de publicidade Yahoo Gemini Ads com mais de cem mil solicitações por segundo para veicular anúncios para 1 bilhão de usuários ativos por mês. Isso nos deu a confiança de que era uma opção testada em batalha, eficiente e confiável - na verdade, a Vespa já existia antes mesmo do Elasticsearch.



Além disso, os desenvolvedores da Vespa provaram ser muito sociáveis ​​e prestativos. A Vespa foi originalmente construída para publicidade e conteúdo. Pelo que sabemos, ainda não foi usado em sites de namoro. Foi difícil integrar o motor no início porque tínhamos um caso de uso único, mas a equipe da Vespa provou ser muito responsiva e otimizou rapidamente o sistema para nos ajudar a lidar com vários problemas que surgiram.



Como o Vespa funciona e como é a pesquisa no OkCupid







Antes de mergulhar em nosso exemplo Vespa, aqui está uma visão geral rápida de como funciona. Vespa é uma coleção de vários serviços, mas cada contêiner Docker pode ser configurado para ser um host admin / config, um host de contêiner Java sem estado e / ou um host de conteúdo C ++ com estado. O pacote de aplicativos com configuração, componentes, modelo de ML, etc. pode ser implantado via API de estadoem um cluster de configuração que lida com a aplicação de mudanças ao cluster de contêiner e conteúdo. As solicitações de feed e outras solicitações passam por um contêiner Java sem estado (que permite a personalização do processamento) sobre HTTP antes que as atualizações de feed cheguem ao cluster de conteúdo ou as solicitações sejam bifurcadas para a camada de conteúdo onde ocorre a execução de solicitação distribuída. Na maioria das vezes, a implantação de um novo pacote de aplicativo leva apenas alguns segundos e a Vespa processa essas mudanças em tempo real no contêiner e no cluster de conteúdo, de modo que raramente você precisa reiniciar alguma coisa.



Qual é a aparência da pesquisa?



Os documentos do cluster Vespa contêm uma variedade de atributos específicos do usuário. A definição do esquema define os campos de tipo de documento, bem como os perfis de classificação que contêm o conjunto de expressões de classificação aplicáveis. Suponha que temos uma definição de esquema que representa um usuário 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
        }
    }
    
}


A notação indexing: attributeindica que esses campos devem ser armazenados na memória para fornecer o melhor desempenho de leitura e gravação para esses campos.



Digamos que preenchemos o cluster com esses documentos personalizados. Podemos então filtrar e classificar em qualquer um dos campos acima. Por exemplo, fazer uma solicitação POST ao mecanismo de pesquisa padrão http://localhost:8080/search/para encontrar usuários que não sejam nosso próprio usuário 777, dentro de 50 milhas de nossa localização, que estejam online desde o carimbo de data / hora 1592486978, classificados pela última atividade e mantendo os dois principais candidatos. Vamos também selecionar os recursos de resumo para ver a contribuição de cada expressão de classificação em nosso perfil de classificação:



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


Poderíamos obter um 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
                    }
                }
            }
        ]
    }
}


Depois de filtrar pela correspondência da expressão calculada de classificação de resultados da primeira fase (primeira fase) para classificar os resultados. A relevância retornado é a pontuação geral como resultado de executar todas as funções de classificação da primeira fase no rank-perfil que especificado em nossa consulta, isto é ranking.profile myRankProfile. ranking.featuresDefinimos query(lastOnlineWeight)50 na lista , que é então referenciada pela única expressão de classificação que usamos lastOnlineScore. Ele usa uma função de classificação integrada freshness , que é um número próximo a 1 se o carimbo de data / hora no atributo for mais recente que o carimbo de data / hora atual. Desde que tudo esteja indo bem, não há nada complicado aqui.



Ao contrário do conteúdo estático, esse conteúdo pode influenciar se ele é mostrado ao usuário ou não. Por exemplo, eles podem gostar de você! Poderíamos indexar um campo ponderado likedUserSet para cada documento do usuário que contém como chaves os IDs dos usuários de que gostaram e como valores o carimbo de data / hora de quando isso aconteceu. Então seria fácil filtrar aqueles que gostaram de você (por exemplo, adicionando uma expressão likedUserSet contains \”777\”em YQL), mas como incluir essas informações durante a classificação? Como aumentar o togr do usuário que gostou da nossa pessoa nos resultados?



Nos resultados anteriores, a expressão de classificação incomingLikeScoreera 0 para ambas as ocorrências. O usuário 6888497210242094612realmente gostou do usuário777mas atualmente não está disponível no ranking, mesmo se tivéssemos colocado "query(incomingLikeWeight)": 50. Podemos usar a função de classificação em YQL (o primeiro e apenas o primeiro argumento para a função rank()determina se o documento é uma correspondência, mas todos os argumentos são usados ​​para calcular a pontuação de classificação) e, em seguida, usar dotProduct em nossa expressão de classificação YQL para armazenar e recuperar as pontuações brutas (neste caso timestamps quando o usuário gostou de nós), por exemplo, desta forma:



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


Agora o usuário é 68888497210242094612elevado ao topo, já que gostou do nosso usuário e incomingLikeScoretem um significado completo. Claro, nós realmente temos um carimbo de data / hora quando ele gostou de nós para que possamos usá-lo em expressões mais complexas, mas por enquanto, vamos deixar simples.



Isso demonstra a mecânica de filtragem e classificação dos resultados usando um sistema de classificação. A estrutura de classificação fornece uma maneira flexível de aplicar expressões (que são em sua maioria apenas matemáticas) para correspondências durante uma consulta.



Configurando middleware em Java



E se quiséssemos seguir um caminho diferente e tornar essa expressão dotProduct implicitamente parte de cada solicitação? É aqui que entra a camada de contêiner Java customizada - podemos escrever um componente Searcher customizado . Isso permite que você processe parâmetros arbitrários, reescreva a consulta e processe os resultados de uma maneira específica. Aqui está um exemplo em 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)
    }
}


Então, em nosso arquivo services.xml , podemos configurar este componente da seguinte maneira:



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


Em seguida, apenas criamos e implantamos o pacote do aplicativo e fazemos uma solicitação ao manipulador 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"
    }
}


Obtivemos os mesmos resultados de antes! Observe que, no código Kotlin, adicionamos um traceback para gerar a visualização YQL após a alteração, portanto, se definido tracelevel=2nos parâmetros de URL, a resposta também será mostrada:



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


O contêiner de middleware Java é uma ferramenta poderosa para adicionar lógica de processamento customizada por meio do Searcher ou geração nativa de resultados usando o Renderer . Nós personalizamos nossos componentes do Searcherpara lidar com casos como os acima e outros aspectos que queremos tornar implícitos em nossas pesquisas. Por exemplo, um dos conceitos de produto que apoiamos é a ideia de "reciprocidade" - você pode pesquisar usuários com critérios específicos (como faixa etária e distância), mas também deve atender aos critérios de pesquisa de candidatos. Para oferecer suporte a isso em nosso componente Searcher, poderíamos buscar o documento do usuário que está pesquisando para fornecer alguns de seus atributos em uma consulta bifurcada subsequente para filtragem e classificação. A estrutura de classificação e o middleware customizado juntos fornecem uma maneira flexível de oferecer suporte a vários casos de uso. Nestes exemplos, cobrimos apenas alguns aspectos, mas aqui você pode encontrar documentação detalhada.



Como construímos um cluster Vespa e o colocamos em produção



Na primavera de 2019, começamos a planejar um novo sistema. Durante esse tempo, também entramos em contato com a equipe da Vespa e consultamos regularmente sobre nossos casos de uso. Nossa equipe de operações avaliou e construiu a configuração inicial do cluster, e a equipe de back-end começou a documentar, projetar e prototipar vários casos de uso da Vespa.



Os primeiros estágios da prototipagem



Os sistemas backend do OkCupid são escritos em Golang e C ++. Para escrever componentes lógicos Vespa personalizados, bem como fornecer altas taxas de feed usando a API do cliente de feed HTTP Java Vespa , tivemos que nos familiarizar um pouco com o ambiente JVM - acabamos usando Kotlin ao configurar componentes Vespa e em nossos pipelines de feed.



Demorou vários anos para portar a lógica do aplicativo e revelar as funções da Vespa, consultando a equipe da Vespa conforme necessário. A maior parte da lógica do sistema do mecanismo de correspondência é escrita em C ++, portanto, também adicionamos lógica para traduzir nosso filtro atual e classificar o modelo de dados em consultas YQL equivalentes que emitimos para o cluster Vespa via REST. No início, também cuidamos de criar um bom pipeline para preencher novamente o cluster com uma base completa de usuários de documentos; A prototipagem deve envolver muitas mudanças para determinar os tipos de campo corretos a serem usados ​​e, inadvertidamente, requer o reenvio do feed de documentos.



Monitoramento e teste de estresse



Quando criamos o cluster de pesquisa Vespa, tivemos que nos certificar de duas coisas: que ele pode lidar com o volume esperado de consultas de pesquisa e registros, e que as recomendações que o sistema fornece são comparáveis ​​em qualidade ao sistema de emparelhamento existente.



Antes dos testes de carga, adicionamos as métricas do Prometheus em todos os lugares. Vespa-exporter fornece toneladas de estatísticas, e a própria Vespa também fornece um pequeno conjunto de métricas adicionais . Com base nisso, criamos vários painéis Grafana para solicitações por segundo, latência, utilização de recursos por processos Vespa, etc. Também executamos o vespa-fbench para testar o desempenho da consulta. Com a ajuda dos desenvolvedores de Vespa, determinamos que devido ao relativamente altocusto de solicitações estáticas, nosso layout pronto agrupado fornecerá resultados mais rápidos. Em um layout simples, adicionar mais nós basicamente reduz apenas o custo de uma consulta dinâmica (ou seja, a parte da consulta que depende do número de documentos indexados). Um layout agrupado significa que cada grupo de sites configurado conterá um conjunto completo de documentos e, portanto, um grupo pode atender à solicitação. Devido ao alto custo das consultas estáticas, mantendo o mesmo número de nós, aumentamos significativamente a taxa de transferência, aumentando o número de um grupo simples para três. Por fim, também testamos o "tráfego sombra" não relatado em tempo real, quando nos tornamos confiantes na confiabilidade dos benchmarks estáticos.



Otimizando o desempenho



O desempenho do checkout foi um dos maiores obstáculos que enfrentamos no início. No início, tivemos problemas para processar atualizações, mesmo em 1000 QPS (solicitações por segundo). Usamos campos de conjunto ponderado extensivamente, mas eles não foram eficazes no início. Felizmente, os desenvolvedores da Vespa foram rápidos em ajudar a resolver esses problemas, assim como outros relacionados à disseminação de dados. Posteriormente, eles também adicionaram uma extensa documentação sobre o dimensionamento do feed , que usamos até certo ponto: campos inteiros em grandes conjuntos ponderados, quando possível, permitem lote por configuraçãovisibility-delayusando várias atualizações condicionais e contando com campos de atributo (ou seja, na memória), bem como reduzindo o número de pacotes de ida e volta de clientes compactando e mesclando operações em nossos pipelines fmdov. Agora os pipelines estão processando silenciosamente 3.000 QPS em estado estável, e nosso humilde cluster está processando 11 mil atualizações de QPS quando tal pico ocorre por algum motivo.



Qualidade das recomendações



Depois de nos convencermos de que o cluster pode suportar a carga, foi necessário verificar se a qualidade das recomendações não é pior do que no sistema existente. Qualquer pequeno desvio na implementação da classificação tem um grande impacto na qualidade geral das recomendações e no ecossistema geral como um todo. Aplicamos um sistema experimentalVespa em alguns grupos de teste, enquanto o grupo de controle continuou a usar o sistema existente. Várias métricas de negócios foram então analisadas, repetindo e documentando os problemas até que o grupo Vespa tivesse um desempenho tão bom, senão melhor, do que o grupo de controle. Uma vez que estávamos confiantes nos resultados da Vespa, foi fácil encaminhar solicitações de correspondência para o cluster da Vespa. Conseguimos lançar todo o tráfego de pesquisa no cluster Vespa sem problemas!



Diagrama do sistema



De forma simplificada, o diagrama final da arquitetura do novo sistema se parece com este:







Como a Vespa funciona agora e o que vem a seguir



Vamos comparar o estado do localizador de par Vespa com o sistema anterior:



  • Atualizações de esquema

    • Antes: uma semana com centenas de novas linhas de código, implantação cuidadosamente coordenada com vários subsistemas

    • :
  • /

    • :

    • : . , !


    • : ,

    • : , Vespa . -


No geral, o aspecto de design e manutenção do cluster Vespa ajudou no desenvolvimento de todos os produtos OkCupid. No final de janeiro de 2020, colocamos em produção nosso cluster Vespa e ele atende a todas as recomendações na busca de pares. Também adicionamos dezenas de novos campos, expressões de classificação e casos de uso com suporte para todos os novos recursos deste ano, como Pilhas . E, ao contrário do nosso sistema de combinação anterior, agora usamos modelos de aprendizado de máquina em tempo real no momento da consulta.



Qual é o próximo?



Para nós, uma das principais vantagens do Vespa é o suporte direto para classificação usando tensores e integração com modelos treinados usando frameworks como TensorFlow . Esse é um dos principais recursos que estaremos desenvolvendo nos próximos meses. Já estamos usando tensores para alguns casos de uso e, em breve, procuraremos integrar diferentes modelos de aprendizado de máquina que, esperamos, possam prever melhor os resultados e as correspondências para nossos usuários.



Além disso, a Vespa anunciou recentemente o suporte para índices multidimensionais de vizinhos mais próximos, que são totalmente em tempo real, pesquisáveis ​​simultaneamente e atualizados dinamicamente. Estamos muito interessados ​​em explorar outros casos de uso para pesquisa de índice de vizinho mais próximo em tempo real.



OkCupid e Vespa. Ir!



Muitas pessoas já ouviram ou trabalharam com o Elasticsearch, mas não existe uma comunidade tão grande em torno da Vespa. Acreditamos que muitos outros aplicativos Elasticsearch funcionariam melhor no Vespa. É ótimo para o OkCupid e estamos felizes por ter mudado para ele. Essa nova arquitetura nos permitiu evoluir e desenvolver novos recursos com muito mais rapidez. Somos uma empresa relativamente pequena, então é ótimo não se preocupar muito com a complexidade do serviço. Agora estamos muito mais preparados para expandir nosso mecanismo de pesquisa. Sem a Vespa, certamente não poderíamos ter feito o progresso que fizemos no ano passado. Para obter mais informações sobre as capacidades técnicas da Vespa, certifique-se de verificar o Vespa AI em Diretrizes de comércio eletrônico de @jobergum .



Demos o primeiro passo e gostamos dos desenvolvedores da Vespa. Eles nos enviaram uma mensagem de volta e acabou por ser uma coincidência! Não poderíamos ter feito isso sem a ajuda da equipe da Vespa. Agradecimentos especiais a @jobergum e @geirst por recomendações sobre classificação e tratamento de consultas, e @kkraune e @vekterli por seu apoio. O nível de suporte e esforço que a equipe da Vespa nos deu tem sido realmente incrível - desde uma visão profunda do nosso caso de uso até o diagnóstico de problemas de desempenho e melhorias imediatas no motor da Vespa. O camarada @vekterli até voou para nosso escritório em Nova York e trabalhou diretamente conosco por uma semana para ajudar a integrar o motor. Muito obrigado à equipe Vespa!



Em conclusão, abordamos apenas alguns aspectos do uso da Vespa, mas nada disso teria sido possível sem o tremendo trabalho de nossas equipes de back-end e operações no ano passado. Encontramos muitos desafios exclusivos para preencher a lacuna entre os sistemas existentes e a pilha de tecnologia mais moderna, mas esses são tópicos para outros artigos.



All Articles