diff --git a/.changeset/facets-count-optimization.md b/.changeset/facets-count-optimization.md new file mode 100644 index 0000000000..5c251b8aef --- /dev/null +++ b/.changeset/facets-count-optimization.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Simplified the entity facets aggregation from `COUNT(DISTINCT entity_id)` to `COUNT(*)`. The unique constraint on `(entity_id, key, value)` guarantees each entity appears at most once per search row group, making the `DISTINCT` unnecessary. This allows the database to use a simpler aggregation plan. diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 241af26541..5d34ebe3f1 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -2549,18 +2549,18 @@ describe('DefaultEntitiesCatalog', () => { }, ]); - await expect( - catalog.facets({ - facets: ['spec.type'], - query: { kind: 'component' }, - credentials: mockCredentials.none(), - }), - ).resolves.toEqual({ + const result = await catalog.facets({ + facets: ['spec.type'], + query: { kind: 'component' }, + credentials: mockCredentials.none(), + }); + expect(result.facets['spec.type']).toHaveLength(2); + expect(result).toEqual({ facets: { - 'spec.type': [ + 'spec.type': expect.arrayContaining([ { value: 'library', count: 1 }, { value: 'service', count: 1 }, - ], + ]), }, }); }, @@ -2590,18 +2590,18 @@ describe('DefaultEntitiesCatalog', () => { }, ]); - await expect( - catalog.facets({ - facets: ['kind'], - query: { kind: { $in: ['component', 'api'] } }, - credentials: mockCredentials.none(), - }), - ).resolves.toEqual({ + const result = await catalog.facets({ + facets: ['kind'], + query: { kind: { $in: ['component', 'api'] } }, + credentials: mockCredentials.none(), + }); + expect(result.facets.kind).toHaveLength(2); + expect(result).toEqual({ facets: { - kind: [ + kind: expect.arrayContaining([ { value: 'API', count: 1 }, { value: 'Component', count: 1 }, - ], + ]), }, }); }, @@ -2687,10 +2687,10 @@ describe('DefaultEntitiesCatalog', () => { }), ).resolves.toEqual({ facets: { - 'metadata.name': [ + 'metadata.name': expect.arrayContaining([ { value: 'one', count: 1 }, { value: 'two', count: 1 }, - ], + ]), }, }); }, diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 35374cfd15..0d896ca6f4 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -691,9 +691,10 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { .select({ facet: 'search.key', value: 'search.original_value', - count: this.database.raw('count(DISTINCT search.entity_id)'), + count: this.database.raw('count(*)'), }) - .groupBy(['search.key', 'search.original_value']); + .groupBy(['search.key', 'search.original_value']) + .orderBy(['search.key', 'search.original_value']); if (request.filter || request.query) { // Build a subquery that finds matching entity IDs via