export const meta = {
  title: `Search and result aggregations`,
  description: `Add authorization rules to your GraphQL schema to control access to your data.`,
};

<MigrationAlert url={"/cli-legacy/graphql-transformer/searchable"}/>

Add the `@searchable` directive to an `@model` type to enable OpenSearch-based data search and result aggregations. This gives you the ability to:
- search for data using advanced filters, such as substring matching, wildcards, regex, `and`/`or`/`not` conditions
- get aggregation values, such as sum, average, min, max, terms
- retrieve total search result count
- sort the search results across one or multiple fields

```graphql
type Student @model @searchable {
  name: String
  dateOfBirth: AWSDate
  email: AWSEmail
  examsCompleted: Int
}
```

> Once the `@searchable` directive is added, all new records added to the model are streamed to OpenSearch. To backfill existing data, see [Backfill OpenSearch index from DynamoDB table](/cli/graphql/troubleshooting#backfill-opensearch-index-from-dynamodb-table).

## Search and filter data

Every model with a `@searchable` directive attached generates a new "search" GraphQL query to search and filter for records. The example above provides you the ability to search for "Student" records using a "searchStudents" query.  

The `filter` parameter allows you to filter for records based on their field values. 

```graphql
query SearchStudentsByEmail {
  searchStudents(filter: { name: { eq: "Rene Brandel" }}) {
    items {
      id
      name
      email
    }
  }
}
```

In the example above, the search result consists of students with the name "Rene Brandel"

### Supported search operations

|Field type|Supported search operations|
|-|-|
|String|ne, eq, match, matchPhrase, matchPhrasePrefix, multiMatch, exists, wildcard, regexp|
|Int|ne, gt, lt, gte, lte, eq, range|
|Float|ne, gt, lt, gte, lte, eq, range|
|Boolean|eq, ne|
|Enum|ne, eq, match, matchPhrase, matchPhrasePrefix, multiMatch, exists, wildcard, regexp|

### Nested search conditions (and, or, not)
Use the filter parameter to pass a nested `and`/`or`/`not` condition. 

```graphql
query MyQuery {
  searchStudents(filter: {
    name: {wildcard: "*Brandel"}
    or: [
      { dateOfBirth: { lt: "2000-01-01" } },
      { email: { exists: true } }
    ]
  }) {
    items {
      id
      name
      email
      dateOfBirth
    }
  }
}
```

By default, every operation in the filter properties is `and`ed. Use the `or` or `not` properties in the search query's `filter` parameter to override this behavior. 

The query above returns a "Student" if:
- their name ends with "Brandel"
- `and`
  - their date of birth is earlier than 2000-01-01
  - `or`
  - their email exists. 

## Sort search results

Use the `sort` parameter to sort your search results by a field in ascending or descending order. The `field` argument accepts any field available on the model. The `direction` accepts either `asc` or `desc`. 

```graphql
query SearchAndSort {
  searchStudents(
    filter: { name: { wildcard: "*Brandel" } },
    sort: { direction: desc, field: name }
  ) {
    items {
      name
      id
    }
  }
}
```

In the example above, the search result is sorted based on their `name` in a `desc`ending order.

### Sort search result over multiple fields

To sort over multiple fields, provide array of sort conditions. When sorting over multiple fields, the sort conditions are applied in the `sort` array's order.

```graphql
query SearchAndSort {
  searchStudents(
    filter: {name: {wildcard: "*Brandel"}},
    sort: [
      {field: name, direction: desc}, # Sort condition #1
      {field: dateOfBirth, direction: asc} # Sort condition #2
    ]  
  ) {
    items {
      id
      name
      dateOfBirth
    }
  }
}
```

In the example above, the search result is first sorted by `name` in a `desc`ending order and then by `dateOfBirth` in an `asc`ending order.

## Paginate over search results

By default, the search result page size is 100. To customize the page size modify the `limit` parameter. Query for the `nextToken` and use it in your subsequent pagination requests:

```
query MyQuery {
  searchTodos(nextToken: "<YOUR_NEXT_TOKEN>") { # Pass in your nextToken in query
    items {
      description
      id
      name
      createdAt
    }
    nextToken # Next token to paginate on
  }
}
```

## Total count of search results

Add the `total` field in your query response to get the total count of search result hits.

```graphql
query MyQuery {
  searchStudents(filter: {name: {wildcard: "*Brandel"}}) {
    items {
      id
    }
    total # Specify to get total counts
  }
}
```

In the example above, the response's `total` field contains the total search result count for "Students" whose name ends with "Brandel".
Note: `total` is calculated based on all records, irrespective of pagination configurations.

## Aggregate values for search result (minimum, maximum, average, sum, terms)

Use the `aggregates` parameter to get aggregate values such as "minimum", "maximum", "average", and "sum" returned in the `aggregateItems` field.
Note: `aggregates` are calculated based on all records, irrespective of pagination configurations.

<BlockSwitcher>
<Block name="Minimum (min)">

Provide the `min` value as the `aggregate` `type` and specify the `aggregateItems` in the response field.

```graphql
query MyQuery {
  searchStudents(
    aggregates: {
      type: min, # Specifies that you want the "min" value
      field: examsCompleted, # Specifies the field for the aggregate value
      name: "minimumExams" # provides a name to reference in the response field 
    } filter: { name: {wildcard: "Rene*"}}) {
    aggregateItems {
      name
      result {
        ... on SearchableAggregateScalarResult {
          value
        }
      }
    }
  }
}
```

In the example above, the response includes the minimum value of "examsCompleted" for all Students whose name starts with "Rene".

```graphql
{
  "data": {
    "searchStudents": {
      "aggregateItems": [{
        "name": "minimumExams",
        "result": {
          "value": 7
        }
      }]
    }
  }
}
```
</Block>

<Block name="Maximum (max)">

Provide the `max` value as the `aggregate` `type` and specify the `aggregateItems` in the response field.

```graphql
query MyQuery {
  searchStudents(
    aggregates: {
      type: max, # Specifies that you want the "max" value
      field: examsCompleted, # Specifies the field for the aggregate value
      name: "maximumExams" # provides a name to reference in the response field 
    } filter: { name: {wildcard: "Rene*"}}) {
    aggregateItems {
      name
      result {
        ... on SearchableAggregateScalarResult {
          value
        }
      }
    }
  }
}
```

In the example above, the response includes the maximum value of "examsCompleted" for all Students whose name starts with "Rene".

```graphql
{
  "data": {
    "searchStudents": {
      "aggregateItems": [{
        "name": "maximumExams",
        "result": {
          "value": 28
        }
      }]
    }
  }
}
```
</Block>

<Block name="Average (avg)">

Provide the `avg` value as the `aggregate` `type` and specify the `aggregateItems` in the response field.

```graphql
query MyQuery {
  searchStudents(
    aggregates: {
      type: avg, # Specifies that you want the "avg" value
      field: examsCompleted, # Specifies the field for the aggregate value
      name: "averageExams" # provides a name to reference in the response field 
    } filter: { name: {wildcard: "Rene*"}}) {
    aggregateItems {
      name
      result {
        ... on SearchableAggregateScalarResult {
          value
        }
      }
    }
  }
}
```

In the example above, the response includes the average value of "examsCompleted" for all Students whose name starts with "Rene".
```graphql
{
  "data": {
    "searchStudents": {
      "aggregateItems": [{
        "name": "averageExams",
        "result": {
          "value": 17.3
        }
      }]
    }
  }
}
```

</Block>

<Block name="Sum (sum)">

Provide the `sum` value as the `aggregate` `type` and specify the `aggregateItems` in the response field.

```graphql
query MyQuery {
  searchStudents(
    aggregates: {
      type: sum, # Specifies that you want the "sum" value
      field: examsCompleted, # Specifies the field for the aggregate value
      name: "examsSum" # provides a name to reference in the response field 
    } filter: { name: {wildcard: "Rene*"}}) {
    aggregateItems {
      name
      result {
        ... on SearchableAggregateScalarResult {
          value
        }
      }
    }
  }
}
```

In the example above, the response includes the sum of all "examsCompleted" values for all Students whose name starts with "Rene".

```graphql
{
  "data": {
    "searchStudents": {
      "aggregateItems": [{
        "name": "examsSum",
        "result": {
          "value": 392
        }
      }]
    }
  }
}
```

</Block>
<Block name="Terms">

Provide the `terms` value as the `aggregate` `type` and specify the `aggregateItems` in the response field.

```graphql
query MyQuery {
  searchTodos(aggregates: {
    field: description,
    type: terms,
    name: "descriptionTerms"
  }) {
    aggregateItems {
      result {
        ... on SearchableAggregateBucketResult {
          __typename
          buckets {
            doc_count
            key
          }
        }
      }
      name
    }
  }
}

```

In the example above, the response includes the terms for the description and their count:

```graphql
{
  "data": {
    "searchTodos": {
      "aggregateItems": [
        {
          "result": {
            "__typename": "SearchableAggregateBucketResult",
            "buckets": [{
                "doc_count": 2,
                "key": "Shopping list"
              }, {
                "doc_count": 1,
                "key": "Me next todo"
              }]
          },
          "name": "descriptionTerms"
        }
      ]
    }
  }
}
```

</Block>
</BlockSwitcher>

## Set up OpenSearch for production environments
By default, Amplify CLI will configure a t2.small instance type. This is great for getting started and prototyping but not recommended to be used in the production environment per the [OpenSearch best practices](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/bp.html). 

To configure the OpenSearch instance type per environment:
1. Run `amplify env add` to create a new environment (e.g. "prod")
2. Edit the `amplify/team-provider-info.json` file and set `OpenSearchInstanceType` to the instance type that works for your application
```json
{
  "dev": {
    "categories": {
      "api": {
        "<your-api-name>" : {
          "OpenSearchInstanceType": "t2.small.elasticsearch"
        }
      }
    }
  },
  "prod": {
    "categories": {
      "api": {
        "<your-api-name>" : {
          "OpenSearchInstanceType": "t2.medium.elasticsearch"
        }
      }
    }
  }
}
```
3. Deploy your changes with `amplify push`

Learn more about Amazon OpenSearch Service instance types [here](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html).

## How it works
The `@searchable` directive streams the data of an @model type to Amazon OpenSearch Service and configures search resolvers to query against OpenSearch.

Type definition of the `@searchable` directive:
```graphql
# Streams data from DynamoDB to OpenSearch and exposes search capabilities.
directive @searchable(queries: SearchableQueryMap) on OBJECT
input SearchableQueryMap { search: String }
```
