Reconstruindo a Pesquisa de Issues no GitHub: Arquitetura e Desafios
Aprimorando a Pesquisa de Issues: Uma Jornada de Engenharia
Na AITY, compreendemos a importância de sistemas robustos e eficientes, especialmente aqueles que são a espinha dorsal da colaboração e produtividade. Recentemente, acompanhamos de perto uma iniciativa fascinante: a reconstrução da pesquisa de Issues do GitHub. Anteriormente limitada a uma estrutura simples e plana, a pesquisa de Issues agora oferece uma sintaxe avançada com operadores lógicos AND/OR e aninhamento de parênteses. Esta melhoria capacita os desenvolvedores a refinar suas buscas, encontrando exatamente o conjunto de issues de seu interesse.
A implementação dessa funcionalidade, embora altamente requisitada, não foi trivial. Envolveu desafios significativos, como garantir a compatibilidade retroativa com buscas existentes, manter a performance sob um volume intenso de consultas e criar uma experiência de usuário intuitiva para as novas funcionalidades de aninhamento. Neste artigo, vamos mergulhar nos bastidores dessa transformação, explorando como uma funcionalidade tão crítica foi concebida e levada à produção.
A Evolução da Pesquisa de Issues
Originalmente, a pesquisa de Issues do GitHub era baseada em uma lista plana de campos e termos, implicitamente unidos por um operador lógico AND. Por exemplo, a query assignee:@me label:support new-project buscava todas as issues atribuídas a mim E com o rótulo "support" E contendo o texto "new-project".
No entanto, a comunidade de desenvolvedores demandava maior flexibilidade há quase uma década. A capacidade de usar um operador OR entre valores, como label:support OR label:question, era frequentemente solicitada. Em 2021, o GitHub atendeu parcialmente a essa demanda, permitindo buscas no estilo OR com listas de valores separadas por vírgulas para o campo de rótulos. Contudo, a necessidade de aplicar essa flexibilidade a todos os campos de issue persistia, impulsionando a equipe a iniciar uma reformulação completa.
Arquitetura Técnica e Implementação
Do ponto de vista arquitetônico, a mudança central envolveu a substituição do módulo de pesquisa existente para Issues (IssuesQuery) por um novo módulo (ConditionalIssuesQuery). Este novo módulo foi projetado para lidar com queries aninhadas, ao mesmo tempo em que mantinha o suporte aos formatos de query existentes, garantindo a compatibilidade.
A tarefa principal foi reescrever o IssueQuery, o módulo de pesquisa responsável por analisar as strings de consulta e mapeá-las para queries Elasticsearch. O processo de execução de uma busca envolve três estágios de alto nível:
- Parse: Quebrar a string de entrada do usuário em uma estrutura mais fácil de processar (como uma lista ou uma árvore).
- Query: Transformar a estrutura analisada em um documento de query Elasticsearch e executá-la.
- Normalize: Mapear os resultados JSON obtidos do Elasticsearch para objetos Ruby e podar os resultados para remover registros que foram excluídos do banco de dados.
O estágio de Normalize permaneceu inalterado. Concentraremos nossa análise nos estágios de Parse e Query.
Estágio de Parse: Da Lista Plana à Árvore de Sintaxe Abstrata
A string de entrada do usuário, a frase de busca, é primeiramente analisada em uma estrutura intermediária. Ela pode incluir termos de query (palavras-chave) e filtros de busca (critérios de restrição).
Exemplos de frases de busca:
* is:issue assignee:@me codespaces
* assignee:@me label:documentation
Método de Parsing Antigo: Lista Plana Quando apenas queries simples e planas eram suportadas, a análise da string em uma lista de termos e filtros era suficiente para o processamento.
Novo Método de Parsing: Árvore de Sintaxe Abstrata (AST)
Para suportar queries aninhadas e recursivas, a abordagem de lista plana não era mais adequada. A equipe migrou para a análise da string de busca em uma Árvore de Sintaxe Abstrata (AST) usando a biblioteca de parsing parslet. Uma gramática (PEG - Parsing Expression Grammar) foi definida para representar a estrutura da string de busca, abrangendo tanto a sintaxe antiga quanto a nova, garantindo a compatibilidade retroativa.
Um exemplo simplificado de uma gramática para expressões booleanas usando parslet é apresentado a seguir:
class Parser < Parslet::Parser
rule(:space) { match[" "].repeat(1) }
rule(:space?) { space.maybe }
rule(:lparen) { str("(") >> space? }
rule(:rparen) { str(")") >> space? }
rule(:and_operator) { str("and") >> space? }
rule(:or_operator) { str("or") >> space? }
rule(:var) { str("var") >> match["0-9"].repeat(1).as(:var) >> space? }
# A regra primária lida com parênteses.
rule(:primary) { lparen >> or_operation >> rparen | var }
# Note que as regras a seguir são ambas recursivas à direita.
rule(:and_operation) {
(primary.as(:left) >> and_operator >>
and_operation.as(:right)).as(:and) |
primary }
rule(:or_operation) {
(and_operation.as(:left) >> or_operator >>
or_operation.as(:right)).as(:or) |
and_operation }
# Começamos pela regra de menor precedência.
root(:or_operation)
end
Por exemplo, a string de busca is:issue AND (author:deborah-digges OR author:monalisa ) seria analisada na seguinte AST:
{
"root": {
"and": {
"left": {
"filter_term": {
"attribute": "is",
"value": [
{
"filter_value": "issue"
}
]
}
},
"right": {
"or": {
"left": {
"filter_term": {
"attribute": "author",
"value": [
{
"filter_value": "deborah-digges"
}
]
}
},
"right": {
"filter_term": {
"attribute": "author",
"value": [
{
"filter_value": "monalisa"
}
]
}
}
}
}
}
}
}
Estágio de Query: Gerando Documentos Elasticsearch
Uma vez que a query é analisada em uma estrutura intermediária, os próximos passos são transformar essa estrutura em um documento de query que o Elasticsearch entenda e, em seguida, executar essa query.
Geração de Query Antiga: Mapeamento Linear
Cada termo de filtro (ex: label:documentation) possuía uma classe dedicada que sabia como convertê-lo em um trecho de um documento de query Elasticsearch. A geração do documento geral era feita invocando a classe correta para cada termo.
Nova Geração de Query: Travessia Recursiva da AST
A nova abordagem envolve a travessia recursiva da AST gerada no estágio de parsing para construir um documento de query Elasticsearch equivalente. A estrutura aninhada e os operadores booleanos da AST mapeiam-se naturalmente para a query booleana do Elasticsearch, onde AND, OR e NOT correspondem às cláusulas must, should e should_not. Blocos de construção menores para a geração de query foram reutilizados para construir recursivamente o documento de query aninhado durante a travessia da árvore.
Continuando o exemplo da AST anterior, ela seria transformada em um documento de query Elasticsearch semelhante a:
{
"query": {
"bool": {
"must": [
{
"bool": {
"must": [
{
"bool": {
"must": {
"prefix": {
"_index": "issues"
}
}
}
},
{
"bool": {
"should": {
"terms": {
"author_id": [
"<DEBORAH_DIGGES_AUTHOR_ID>",
"<MONALISA_AUTHOR_ID>"
]
}
}
}
}
]
}
}
]
}
// ALGUNS TERMOS OMITIDOS POR BREVIDADE
}
}
Com este novo documento de query, a busca é executada contra o Elasticsearch, suportando agora operadores lógicos AND/OR e parênteses para uma pesquisa mais granular.
Considerações Críticas na Reconstrução
A pesquisa de Issues é uma das funcionalidades mais antigas e intensamente utilizadas do GitHub, com uma média de quase 2000 queries por segundo (QPS). Alterar uma funcionalidade central como essa apresentou diversos desafios:
-
Garantindo Compatibilidade Retroativa:
- As buscas de Issues são frequentemente salvas, compartilhadas e linkadas, sendo artefatos importantes. A nova capacidade deveria ser introduzida sem quebrar as queries existentes.
- Testes extensivos: O novo módulo foi executado contra todos os testes unitários e de integração existentes. Testes nas APIs GraphQL e REST foram realizados com e sem a feature flag ativada.
- Validação em produção com dark-shipping: Para 1% das buscas, ambas as versões do sistema foram executadas em paralelo em background, registrando diferenças nos resultados (inicialmente, o "número de resultados"). Isso permitiu corrigir bugs e casos de borda antes que afetassem os usuários.
-
Prevenção de Degradação de Performance:
- Queries aninhadas mais complexas exigem mais recursos. Foi necessário estabelecer uma linha de base realista e garantir que não houvesse regressão na performance de queries mais simples.
- Comparação em produção: Para 1% das buscas, queries equivalentes foram executadas em ambos os sistemas. A biblioteca
scientistdo GitHub (open-source, Ruby) foi utilizada para comparar a performance e garantir a ausência de regressões.
-
Preservação da Experiência do Usuário (UX):
- A equipe colaborou estreitamente com os times de produto e design para garantir que a usabilidade não diminuísse.
- Limitação de aninhamento: O número de níveis de aninhamento foi limitado a cinco, um ponto ideal entre utilidade e usabilidade.
- Cues visuais e de UX: Palavras-chave AND/OR são destacadas, e o recurso de auto-complete para termos de filtro, já conhecido pelos usuários, foi mantido.
-
Minimização de Risco para Usuários Existentes:
- Para uma funcionalidade usada por milhões de usuários, a implantação precisou ser intencional e gradual.
- Blast radius limitado: Inicialmente, o novo sistema foi integrado apenas na API GraphQL e na aba de Issues de um repositório na UI. Isso permitiu coletar feedback e fazer ajustes.
- Rollout faseado: Após a validação inicial, foi estendido para o dashboard de Issues e para a API REST.
- Testes internos e com parceiros: A funcionalidade foi testada internamente por toda a equipe do GitHub e depois com parceiros confiáveis para obter feedback inicial antes do lançamento geral.
Impacto Prático na Engenharia de Software
A reconstrução da pesquisa de Issues do GitHub é um exemplo brilhante de como a modernização de sistemas legados, mesmo os mais críticos e utilizados, pode ser realizada com sucesso. Na AITY, essa abordagem reforça a importância de princípios de engenharia como a compatibilidade retroativa, a atenção meticulosa à performance e uma forte colaboração entre engenharia, produto e design. Ao adotar uma estratégia de rollout gradual e testes rigorosos em produção, a equipe demonstrou como mitigar riscos significativos e entregar valor substancial ao usuário, transformando uma funcionalidade básica em uma ferramenta poderosa e flexível para milhões de desenvolvedores em todo o mundo. Este caso prático serve como um blueprint valioso para aprimorar qualquer sistema crítico.
Aguardando Login...