Usage
Search
Grouping

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.

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:

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:

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:

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:

{
  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:

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:

// 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.