Skip to content

Grouping

Orama supports groupBy operations. That allows you to group results in groups calculating an aggregation on the item that belongs to the same bucket.

javascript
const results = await search(db, {
  term: 't-shirt',
  groupBy: {
    properties: ['design'], // required: property on which we want to group on
    maxResult: 1, // optional: for every group, how many results we want
    reduce: { // optional: customize the aggregation logic
      reducer: Function,
      getInitialValue: Function
    }
  },
})

By default, Orama doesn't limit the number of items inside a group.

By default, Orama groups all the matched documents into an array.

Simple usage

If we consider the following schema:

javascript
const db = await create({
  schema: {
    id: 'string',
    type: 'string',
    design: 'string',
    color: 'string',
    rank: 'number',
    isPromoted: 'boolean',
  },
})
const ids = await insertMultiple(db, [
  { id: '0', type: 't-shirt', design: 'A', color: 'blue', rank: 3, isPromoted: true },
  { id: '1', type: 't-shirt', design: 'A', color: 'green', rank: 5, isPromoted: false },
  { id: '2', type: 't-shirt', design: 'A', color: 'red', rank: 4, isPromoted: false },
  { id: '3', type: 't-shirt', design: 'B', color: 'blue', rank: 4, isPromoted: false },
  { id: '4', type: 't-shirt', design: 'B', color: 'green', rank: 4, isPromoted: true },
  { id: '5', type: 't-shirt', design: 'B', color: 'white', rank: 5, isPromoted: false },
  { id: '6', type: 't-shirt', design: 'B', color: 'gray', rank: 5, isPromoted: true },
  { id: '7', type: 'sweatshirt', design: 'A', color: 'yellow', rank: 3, isPromoted: true },
  { id: '8', type: 'sweatshirt', design: 'A', color: 'green', rank: 4, isPromoted: false },
])

We will be able to have the documents per design ordered by rank:

javascript
const results = await search(db, {
  term: 't-shirt',
  groupBy: {
    properties: ['design'], // property on which we want to group on
  },
  sortBy: {
    property: 'rank', // inside a group, the result is ordered following this property
    order: 'DESC', // with this order
  },
})

If you want only the top-ranked document per design, you can specify the maxResult:

javascript
const results = await search(db, {
  term: 't-shirt',
  groupBy: {
    properties: ['design'],
    maxResult: 1, // for every group, how many results we want
  },
  sortBy: {
    property: 'rank',
    order: 'DESC',
  },
})

The above query returns something like this:

js
{
  groups: [
    {
      values: ['A'], // list of the values the group is referring to
      result: [
        {
          id: '1',
          score: 0, 
          document: { ... } // the doc with id '1' 
        }
      ]
    },
    {
      values: ['B'], // list of the values the group is referring to
      result: [
        {
          id: '5',
          score: 0, 
          document: { ... } // the doc with id '5'
        }
      ]
    }
  ],
  // The other common properties like `hits` and `elapsed`
}

You can group on multiple properties as follows:

javascript
const results = await search(db, {
  term: 'red t-shirt',
  groupBy: {
    properties: ['design', 'rank', 'isPromoted'], // group on the combination of the values
  },
  sortBy: {
    property: 'id',
    order: 'ASC',
  },
})

Custom reducer

Orama supports custom aggregator as follows:

typescript
// The document interface
interface Doc extends Document {
  type: string
  design: string
  rank: number
  color: string
  isPromoted: boolean
}
// The aggregation interface
interface AggregationValue {
  type: string
  design: string
  colors: string[]
  ranks: number[]
  isPromoted: boolean
}

const results = await search(db, {
  term: 'red t-shirt',
  groupBy: {
    properties: ['type', 'design'], // group on both properties
    reduce: {
      // the accumulator function
      reducer: (values: ScalarSearchableValue[], acc: AggregationValue, item: Result) => {
        const doc = item.document as Doc
        acc.type ||= doc.type
        acc.design ||= doc.design
        acc.isPromoted ||= doc.isPromoted
        acc.colors.push(doc.color)
        acc.ranks.push(doc.rank)
        return acc
      },
      // The initial value: this is called for every group
      getInitialValue: (): AggregationValue => ({ type: '', design: '', colors: [], ranks: [], isPromoted: false }),
    },
  },
  sortBy: {
    property: 'rank',
    order: 'DESC',
  }
})

Where the accumulator function receives the following parameters:

  1. the value of the current groups
  2. the accumulator returned by the previous invocation
  3. the item to accumulate

The reducer is called for every item for every group.