{"id":2015,"date":"2025-05-13T16:16:33","date_gmt":"2025-05-13T16:16:33","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/05\/13\/github-issues-search-now-supports-nested-queries-and-boolean-operators-heres-how-we-rebuilt-it\/"},"modified":"2025-05-13T16:16:33","modified_gmt":"2025-05-13T16:16:33","slug":"github-issues-search-now-supports-nested-queries-and-boolean-operators-heres-how-we-rebuilt-it","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/05\/13\/github-issues-search-now-supports-nested-queries-and-boolean-operators-heres-how-we-rebuilt-it\/","title":{"rendered":"GitHub Issues search now supports nested queries and boolean operators: Here\u2019s how we (re)built it"},"content":{"rendered":"<p>Originally, Issues search was limited by a simple, flat structure of queries. But with <a href=\"https:\/\/github.blog\/changelog\/2025-04-09-evolving-github-issues-and-projects\/#%F0%9F%95%B5%EF%B8%8F%E2%99%80%EF%B8%8F-finding-what-you-need-with-advanced-search\">advanced search syntax<\/a>, you can now construct searches using logical AND\/OR operators and nested parentheses, pinpointing the exact set of issues you care about.<\/p>\n<p>Building this feature presented significant challenges: ensuring backward compatibility with existing searches, maintaining performance under high query volume, and crafting a user-friendly experience for nested searches. We\u2019re excited to take you behind the scenes to share how we took this long-requested feature from idea to production.<\/p>\n<h2 class=\"wp-block-heading\">Here\u2019s what you can do with the new syntax and how it works behind the scenes<\/h2>\n<p>Issues search now supports building queries with logical AND\/OR operators across <em>all<\/em> fields, with the ability to nest query terms. For example is:issue state:open author:rileybroughten (type:Bug OR type:Epic) finds all <em>issues<\/em> that are <em>open<\/em> AND were <em>author<\/em>ed by <em>rileybroughten<\/em> AND are either of type <em>bug<\/em> or <em>epic<\/em>.<\/p>\n<h2 class=\"wp-block-heading\">How did we get here?<\/h2>\n<p>Previously, as mentioned, Issues search only supported a flat list of query fields and terms, which were implicitly joined by a logical AND. For example, the query assignee:@me label:support new-project translated to \u201cgive me all issues that are assigned to me AND have the label <em>support <\/em>AND<em> <\/em>contain the text <em>new-project.<\/em>\u201d<\/p>\n<p>But the developer community has been <a href=\"https:\/\/github.com\/isaacs\/github\/issues\/660\">asking for more flexibility in issue search<\/a>, <a href=\"https:\/\/github.com\/orgs\/community\/discussions\/4507\">repeatedly<\/a>, for nearly a decade now. They wanted to be able to find all issues that had <em>either<\/em> the label support or the label question, using the query label:support OR label:question. So, we shipped an <a href=\"https:\/\/github.blog\/changelog\/2021-08-02-search-issues-by-label-using-logical-or\/\">enhancement towards this request<\/a> in 2021, when we enabled an OR style search using a comma-separated list of values.<\/p>\n<p>However, they still wanted <a href=\"https:\/\/github.com\/orgs\/community\/discussions\/4507#discussioncomment-3076699\">the flexibility to search this way across <em>all<\/em> issue fields<\/a>, and not just the <em>labels<\/em> field. So we got to work.\u00a0<\/p>\n<h2 class=\"wp-block-heading\">Technical architecture and implementation<\/h2>\n<p>From an architectural perspective, we swapped out the existing search module for Issues (IssuesQuery), with a new search module (ConditionalIssuesQuery), that was capable of handling nested queries while continuing to support existing query formats.<\/p>\n<p>This involved rewriting IssueQuery, the search module that parsed query strings and mapped them into Elasticsearch queries.<\/p>\n<p>To build a new search module, we first needed to understand the existing search module, and how a single search query flowed through the system. At a high level, when a user performs a search, there are three stages in its execution:<\/p>\n<p><strong>Parse<\/strong>: Breaking the user input string into a structure that is easier to process (like a list or a tree)<\/p>\n<p><strong>Query<\/strong>: Transforming the parsed structure into an Elasticsearch query document, and making a query against Elasticsearch.<\/p>\n<p><strong>Normalize: <\/strong>Mapping the results obtained from Elasticsearch (JSON) into Ruby objects for easy access and pruning the results to remove records that had since been removed from the database.<\/p>\n<p>Each stage presented its own challenges, which we\u2019ll explore in more detail below. The <em>Normalize<\/em> step remained unchanged during the re-write, so we won\u2019t dive into that one.<\/p>\n<h3 class=\"wp-block-heading\">Parse stage<\/h3>\n<p>The user input string (the search phrase) is first parsed into an intermediate structure. The search phrase could include:<\/p>\n<p><strong>Query terms:<\/strong> The relevant words the user is trying to find more information about (ex: \u201cmodels\u201d)<\/p>\n<p><strong>Search filters: <\/strong>These restrict the set of returned search documents based on some criteria (ex: \u201cassignee:Deborah-Digges\u201d)<\/p>\n<p>\u00a0Example search phrase:\u00a0<\/p>\n<p>Find all issues assigned to me that contain the word \u201ccodespaces\u201d:<\/p>\n<p>is:issue assignee:@me codespaces<\/p>\n<p>Find all issues with the label <em>documentation<\/em> that are assigned to me:<\/p>\n<p>assignee:@me label:documentation<\/p>\n<h4 class=\"wp-block-heading\">The old parsing method: flat list<\/h4>\n<p>When only flat, simple queries were supported, it was sufficient to parse the user\u2019s search string into a list of search terms and filters, which would then be passed along to the next stage of the search process.<\/p>\n<h4 class=\"wp-block-heading\">The new parsing method: abstract syntax tree<\/h4>\n<p>As nested queries may be recursive, parsing the search string into a list was no longer sufficient. We changed this component to parse the user\u2019s search string into an Abstract Syntax Tree (AST) using the parsing library <a href=\"https:\/\/github.com\/kschiess\/parslet\">parslet<\/a>.<\/p>\n<p>We defined a grammar (a PEG or Parsing Expression Grammar) to represent the structure of a search string. The grammar supports both the existing query syntax and the new nested query syntax, to allow for backward compatibility.<\/p>\n<p>A <a href=\"https:\/\/github.com\/kschiess\/parslet\/blob\/master\/example\/boolean_algebra.rb\">simplified grammar<\/a> for a boolean expression described by a PEG grammar for the parslet parser is shown below:<\/p>\n<p>class Parser &lt; Parslet::Parser<br \/>\n  rule(:space)  { match[&#8221; &#8220;].repeat(1) }<br \/>\n  rule(:space?) { space.maybe }<\/p>\n<p>  rule(:lparen) { str(&#8220;(&#8220;) &gt;&gt; space? }<br \/>\n  rule(:rparen) { str(&#8220;)&#8221;) &gt;&gt; space? }<\/p>\n<p>  rule(:and_operator) { str(&#8220;and&#8221;) &gt;&gt; space? }<br \/>\n  rule(:or_operator)  { str(&#8220;or&#8221;)  &gt;&gt; space? }<\/p>\n<p>  rule(:var) { str(&#8220;var&#8221;) &gt;&gt; match[&#8220;0-9&#8221;].repeat(1).as(:var) &gt;&gt; space? }<\/p>\n<p>  # The primary rule deals with parentheses.<br \/>\n  rule(:primary) { lparen &gt;&gt; or_operation &gt;&gt; rparen | var }<\/p>\n<p>  # Note that following rules are both right-recursive.<br \/>\n  rule(:and_operation) {<br \/>\n    (primary.as(:left) &gt;&gt; and_operator &gt;&gt;<br \/>\n      and_operation.as(:right)).as(:and) |<br \/>\n    primary }<\/p>\n<p>  rule(:or_operation)  {<br \/>\n    (and_operation.as(:left) &gt;&gt; or_operator &gt;&gt;<br \/>\n      or_operation.as(:right)).as(:or) |<br \/>\n    and_operation }<\/p>\n<p>  # We start at the lowest precedence rule.<br \/>\n  root(:or_operation)<br \/>\nend<\/p>\n<p>For example, this user search string:<br \/>is:issue AND (author:deborah-digges OR author:monalisa )\u00a0<br \/>would be parsed into the following AST:<\/p>\n<p>{<br \/>\n  &#8220;root&#8221;: {<br \/>\n    &#8220;and&#8221;: {<br \/>\n      &#8220;left&#8221;: {<br \/>\n        &#8220;filter_term&#8221;: {<br \/>\n          &#8220;attribute&#8221;: &#8220;is&#8221;,<br \/>\n          &#8220;value&#8221;: [<br \/>\n            {<br \/>\n              &#8220;filter_value&#8221;: &#8220;issue&#8221;<br \/>\n            }<br \/>\n          ]<br \/>\n        }<br \/>\n      },<br \/>\n      &#8220;right&#8221;: {<br \/>\n        &#8220;or&#8221;: {<br \/>\n          &#8220;left&#8221;: {<br \/>\n            &#8220;filter_term&#8221;: {<br \/>\n              &#8220;attribute&#8221;: &#8220;author&#8221;,<br \/>\n              &#8220;value&#8221;: [<br \/>\n                {<br \/>\n                  &#8220;filter_value&#8221;: &#8220;deborah-digges&#8221;<br \/>\n                }<br \/>\n              ]<br \/>\n            }<br \/>\n          },<br \/>\n          &#8220;right&#8221;: {<br \/>\n            &#8220;filter_term&#8221;: {<br \/>\n              &#8220;attribute&#8221;: &#8220;author&#8221;,<br \/>\n              &#8220;value&#8221;: [<br \/>\n                {<br \/>\n                  &#8220;filter_value&#8221;: &#8220;monalisa&#8221;<br \/>\n                }<br \/>\n              ]<br \/>\n            }<br \/>\n          }<br \/>\n        }<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<h3 class=\"wp-block-heading\">Query<\/h3>\n<p>Once the query is parsed into an intermediate structure, the next steps are to:<\/p>\n<p>Transform this intermediate structure into a query document that Elasticsearch understands<\/p>\n<p>Execute the query against Elasticsearch to obtain results<\/p>\n<p>Executing the query in step 2 remained the same between the old and new systems, so let\u2019s only go over the differences in building the query document below.<\/p>\n<h4 class=\"wp-block-heading\">The old query generation: linear mapping of filter terms using filter classes<\/h4>\n<p>Each filter term (Ex: label:documentation) has a class that knows how to convert it into a snippet of an Elasticsearch query document. During query document generation, the correct class for each filter term is invoked to construct the overall query document.<\/p>\n<h4 class=\"wp-block-heading\">The new query generation: recursive AST traversal to generate Elasticsearch bool query<\/h4>\n<p>We recursively traversed the AST generated during parsing to build an equivalent Elasticsearch query document. The nested structure and boolean operators map nicely to Elasticsearch\u2019s <a href=\"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/reference\/current\/query-dsl-bool-query.html\">boolean query<\/a> with the AND, OR, and NOT operators mapping to the <em>must<\/em>, <em>should<\/em>, and <em>should_not<\/em> clauses.<\/p>\n<p>We re-used the building blocks for the smaller pieces of query generation to recursively construct a nested query document during the tree traversal.<\/p>\n<p>Continuing from the example in the parsing stage, the AST would be transformed into a query document that looked like this:<\/p>\n<p>{<br \/>\n  &#8220;query&#8221;: {<br \/>\n    &#8220;bool&#8221;: {<br \/>\n      &#8220;must&#8221;: [<br \/>\n        {<br \/>\n          &#8220;bool&#8221;: {<br \/>\n            &#8220;must&#8221;: [<br \/>\n              {<br \/>\n                &#8220;bool&#8221;: {<br \/>\n                  &#8220;must&#8221;: {<br \/>\n                    &#8220;prefix&#8221;: {<br \/>\n                      &#8220;_index&#8221;: &#8220;issues&#8221;<br \/>\n                    }<br \/>\n                  }<br \/>\n                }<br \/>\n              },<br \/>\n              {<br \/>\n                &#8220;bool&#8221;: {<br \/>\n                  &#8220;should&#8221;: {<br \/>\n                    &#8220;terms&#8221;: {<br \/>\n                      &#8220;author_id&#8221;: [<br \/>\n                        &#8220;&lt;DEBORAH_DIGGES_AUTHOR_ID&gt;&#8221;,<br \/>\n                        &#8220;&lt;MONALISA_AUTHOR_ID&gt;&#8221;<br \/>\n                      ]<br \/>\n                    }<br \/>\n                  }<br \/>\n                }<br \/>\n              }<br \/>\n            ]<br \/>\n          }<br \/>\n        }<br \/>\n      ]<br \/>\n    }<br \/>\n    \/\/ SOME TERMS OMITTED FOR BREVITY<br \/>\n  }<br \/>\n}<\/p>\n<p>With this new query document, we execute a search against Elasticsearch. This search now supports logical AND\/OR operators and parentheses to search for issues in a more fine-grained manner.<\/p>\n<h2 class=\"wp-block-heading\">Considerations<\/h2>\n<p>Issues is one of the oldest and most heavily -used features on GitHub. Changing core functionality like Issues search, a feature with an average of\u00a0 nearly 2000 queries per second (QPS)\u2014that\u2019s almost 160M queries a day!\u2014presented a number of challenges to overcome.<\/p>\n<h3 class=\"wp-block-heading\">Ensuring backward compatibility<\/h3>\n<p>Issue searches are often bookmarked, shared among users, and linked in documents, making them important artifacts for developers and teams. Therefore, we wanted to introduce this new capability for nested search queries without breaking existing queries for users.\u00a0<\/p>\n<p>We validated the new search system before it even reached users by:<\/p>\n<p><strong>Testing extensively<\/strong>: We ran our new search module against all unit and integration tests for the existing search module. To ensure that the GraphQL and REST API contracts remained unchanged, we ran the tests for the search endpoint both with the feature flag for the new search system enabled and disabled.<\/p>\n<p><strong>Validating correctness in production with dark-shipping: <\/strong>For 1% of issue searches, we ran the user\u2019s search against both the existing and new search systems in a background job, and logged differences in responses. By analyzing these differences we were able to fix bugs and missed edge cases before they reached our users.<\/p>\n<p>We weren\u2019t sure at the outset how to define \u201cdifferences,\u201d but we settled on \u201cnumber of results\u201d for the first iteration. In general, it seemed that we could determine whether a user would be surprised by the results of their search against the new search capability if a search returned a different number of results when they were run within a second or less of each other.<\/p>\n<h3 class=\"wp-block-heading\">Preventing performance degradation<\/h3>\n<p>We expected more complex nested queries to use more resources on the backend than simpler queries, so we needed to establish a realistic baseline for nested queries, while ensuring no regression in the performance of existing, simpler ones.<\/p>\n<p>For 1% of Issue searches, we ran equivalent queries against both the existing and the new search systems. We used <a href=\"https:\/\/github.com\/github\/scientist\">scientist<\/a>, GitHub\u2019s open source Ruby library, for carefully refactoring critical paths, to compare the performance of equivalent queries to ensure that there was no regression.<\/p>\n<h3 class=\"wp-block-heading\">Preserving user experience<\/h3>\n<p>We didn\u2019t want users to have a worse experience than before just because more complex searches were<em> possible<\/em>.\u00a0<\/p>\n<p>We collaborated closely with product and design teams to ensure usability didn\u2019t decrease as we added this feature by:<\/p>\n<p><strong>Limiting the number of nested levels <\/strong>in a query to five. From customer interviews, we found this to be a sweet spot for both utility and usability.<\/p>\n<p><strong>Providing helpful UI\/UX cues<\/strong>: We highlight the AND\/OR keywords in search queries, and provide users with the same auto-complete feature for filter terms in the UI that they were accustomed to for simple flat queries.<\/p>\n<h3 class=\"wp-block-heading\">Minimizing risk to existing users<\/h3>\n<p>For a feature that is used by millions of users a day, we needed to be intentional about rolling it out in a way that minimized risk to users.<\/p>\n<p>We built confidence in our system by:<\/p>\n<p><strong>Limiting blast radius<\/strong>: To gradually build confidence, we only integrated the new system in the GraphQL API and the Issues tab for a repository in the UI to start. This gave us time to collect, respond to, and incorporate feedback without risking a degraded experience for all consumers. Once we were happy with its performance, we rolled it out to the <a href=\"https:\/\/github.blog\/changelog\/2025-04-02-github-issues-dashboard-updates\/\">Issues dashboard<\/a> and the <a href=\"https:\/\/github.blog\/changelog\/2025-03-06-github-issues-projects-api-support-for-issues-advanced-search-and-more\/\">REST API<\/a>.<\/p>\n<p><strong>Testing internally and with trusted partners<\/strong>: As with every feature we build at GitHub, we tested this feature internally for the entire period of its development by shipping it to our own team during the early days, and then gradually rolling it out to all GitHub employees. We then shipped it to trusted partners to gather initial user feedback.<\/p>\n<p>And there you have it, that\u2019s how we built, validated, and shipped the new and improved Issues search!<\/p>\n<h2 class=\"wp-block-heading\">Feedback<\/h2>\n<p>Want to try out this exciting new functionality? Head to our docs to learn about how to use <a href=\"https:\/\/docs.github.com\/en\/issues\/tracking-your-work-with-issues\/using-issues\/filtering-and-searching-issues-and-pull-requests#using-boolean-operators\">boolean operators<\/a> and <a href=\"https:\/\/docs.github.com\/en\/issues\/tracking-your-work-with-issues\/using-issues\/filtering-and-searching-issues-and-pull-requests#using-parentheses-for-more-complicated-filters\">parentheses<\/a> to search for the issues you care about!<\/p>\n<p>If you have any feedback for this feature, please drop us a note on our <a href=\"https:\/\/github.com\/orgs\/community\/discussions\/categories\/announcements\">community discussions<\/a>.<\/p>\n<h2 class=\"wp-block-heading\">Acknowledgements<\/h2>\n<p>Special thanks to AJ Schuster, Riley Broughten, Stephanie Goldstein, Eric Jorgensen Mike Melanson and Laura Lindeman for the feedback on several iterations of this blog post!<\/p>\n\n<p>The post <a href=\"https:\/\/github.blog\/developer-skills\/application-development\/github-issues-search-now-supports-nested-queries-and-boolean-operators-heres-how-we-rebuilt-it\/\">GitHub Issues search now supports nested queries and boolean operators: Here\u2019s how we (re)built it<\/a> appeared first on <a href=\"https:\/\/github.blog\/\">The GitHub Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Originally, Issues search was limited by a simple, flat structure of queries. But with advanced search syntax, you can now [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[8],"tags":[],"class_list":["post-2015","post","type-post","status-publish","format-standard","hentry","category-github-engineering"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/2015","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=2015"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/2015\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=2015"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=2015"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=2015"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}