How to group a complex pg_search query

This is a complex problem to tackle. Let's start with a bit of a background...

It all started with Prosopite telling me that running a count or a sum on a customer's invoices while looping through all customers is a bad practice. N+1 warning!

At this point, I do what any overachieving optimizer does: I set out to fix the problem!

My first attempt was something like this:
lang-ruby
module Bills
  class Customer < ApplicationRecord
    include PgSearch::Model

    has_many :invoices, dependent: :destroy

    scope :with_invoice_totals, -> {
      left_joins(:invoices)
        .select("bills_customers.*")
        .select("COALESCE(SUM(bills_invoices.total_gross_cents), 0) AS total_invoiced_gross_cents")
        .select("COALESCE(SUM(bills_invoices.total_net_cents), 0) AS total_invoiced_net_cents")
        .select("COALESCE(SUM(bills_invoices.total_vat_cents), 0) AS total_invoiced_vat_cents")
        .group('"bills_customers"."id"')
    }
So basically, I select the invoice totals as part of the query, which kills the N+1 warning until... I try to combine this with searching for a customer. The horror!!
ActiveRecord::StatementInvalid - PG::GroupingError: ERROR:  column "pg_search_786fed5704f7001547079e.rank" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ..." = 1))) GROUP BY "bills_customers"."id" ORDER BY pg_search_...
This was vaguely familiar; I remember running into this in the past and never having the time to look into it in detail.

So, let's take a look at our search scopes:
lang-ruby
module Bills
  class Customer < ApplicationRecord
    include PgSearch::Model

    has_many :invoices, dependent: :destroy

    scope :with_invoice_totals, -> {
      left_joins(:invoices)
        .select("bills_customers.*")
        .select("COALESCE(SUM(bills_invoices.total_gross_cents), 0) AS total_invoiced_gross_cents")
        .select("COALESCE(SUM(bills_invoices.total_net_cents), 0) AS total_invoiced_net_cents")
        .select("COALESCE(SUM(bills_invoices.total_vat_cents), 0) AS total_invoiced_vat_cents")
        .group('"bills_customers"."id"')
    }

    pg_search_scope :customer_searchable,
      against: {
        company: "A",
        first_name: "B",
        last_name: "C",
        vat_number: "D",
      },
      using: {
        tsearch: {
          prefix: true,
          dictionary: "english",
          tsvector_column: "customer_searchable",
        },
      }

    pg_search_scope :address_searchable,
      against: {
        street_1: "A",
        postal_code: "B",
        city: "C",
        country: "D",
      },
      using: {
        tsearch: {
          prefix: true,
          dictionary: "english",
          tsvector_column: "address_searchable",
        },
      }

    pg_search_scope :search_customer_and_address,
      against: {
        company: "A",
        first_name: "B",
        last_name: "C",
        vat_number: "D",
        street_1: "B",
        postal_code: "C",
        city: "C",
        country_code: "D",
      },
      using: {
        tsearch: {
          prefix: true,
          dictionary: "english",
          tsvector_column: %w[customer_searchable address_searchable],
        },
      }
To make things even more complicated, and due to limitations in pg_search and tsvector columns, I need to combine two search scopes to search for everything.

I came across this issue: https://github.com/Casecommons/pg_search/issues/367 which led me to this issue: https://github.com/Casecommons/pg_search/issues/238 which finally led me to: https://github.com/Casecommons/pg_search/blob/v1.0.5/lib/pg_search/scope_options.rb#L34C11-L34C30.

So, adding with_pg_search_rank needs to be done in the model so we need another method to combine everything:
lang-ruby
def self.search_and_aggregate(query)
  base_query = with_invoice_totals
  return base_query if query.blank?

  base_query
    .search_customer_and_address(query)
    .with_pg_search_rank
    .group("pg_search_rank")

Now we can use this from the controller like so:
module Bills
  class CustomersController < CrewScopedController
    def index
      query = params[:query]
      customers = Current.crew.invoices.where(user: Current.user) 
      customers = customers.search_and_aggregate(query)

      respond_to do |format|
        format.html do
          render Customers::Index.new(customers:, query:)
        end
        format.turbo_stream do
          component = Customers::Customers.new(customers:)

          render turbo_stream: turbo_stream.replace(
            component.id,
            component
          )
        end
      end
    end
  end
end

The cool thing about Ruby on Rails is that you can do as many group, select, and where you please. Rails appends every single one to the list. If you want to remove the existing ones, you must use regroup, reselect, or rewhere: https://guides.rubyonrails.org/active_record_querying.html#overriding-conditions