Engenharia

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:

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:

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.

Comentários

Interações
Seu Perfil

Aguardando Login...