Page updated Jan 16, 2024

Preview: AWS Amplify's new code-first DX (Gen 2)

The next generation of Amplify's backend building experience with a TypeScript-first DX.

Get started

Build search and aggregate queries

You are currently viewing the new GraphQL transformer v2 docs Looking for legacy docs?

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
1type Student @model @searchable {
2 name: String
3 dateOfBirth: AWSDate
4 email: AWSEmail
5 examsCompleted: Int
6}

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.

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.

1query SearchStudentsByEmail {
2 searchStudents(filter: { name: { eq: "Rene Brandel" } }) {
3 items {
4 id
5 name
6 email
7 }
8 }
9}

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

Supported search operations

Field typeSupported search operations
Stringne, eq, match, matchPhrase, matchPhrasePrefix, multiMatch, exists, wildcard, regexp
Intne, gt, lt, gte, lte, eq, range
Floatne, gt, lt, gte, lte, eq, range
Booleaneq, ne
Enumne, 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.

1query MyQuery {
2 searchStudents(
3 filter: {
4 name: { wildcard: "*Brandel" }
5 or: [{ dateOfBirth: { lt: "2000-01-01" } }, { email: { exists: true } }]
6 }
7 ) {
8 items {
9 id
10 name
11 email
12 dateOfBirth
13 }
14 }
15}

By default, every operation in the filter properties is anded. 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.

1query SearchAndSort {
2 searchStudents(
3 filter: { name: { wildcard: "*Brandel" } }
4 sort: { direction: desc, field: name }
5 ) {
6 items {
7 name
8 id
9 }
10 }
11}

In the example above, the search result is sorted based on their name in a descending 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.

1query SearchAndSort {
2 searchStudents(
3 filter: { name: { wildcard: "*Brandel" } }
4 sort: [
5 { field: name, direction: desc } # Sort condition #1
6 { field: dateOfBirth, direction: asc } # Sort condition #2
7 ]
8 ) {
9 items {
10 id
11 name
12 dateOfBirth
13 }
14 }
15}

In the example above, the search result is first sorted by name in a descending order and then by dateOfBirth in an ascending 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:

1query MyQuery {
2 searchTodos(nextToken: "<YOUR_NEXT_TOKEN>") { # Pass in your nextToken in query
3 items {
4 description
5 id
6 name
7 createdAt
8 }
9 nextToken # Next token to paginate on
10 }
11}

Total count of search results

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

1query MyQuery {
2 searchStudents(filter: { name: { wildcard: "*Brandel" } }) {
3 items {
4 id
5 }
6 total # Specify to get total counts
7 }
8}

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.

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

1query MyQuery {
2 searchStudents(
3 aggregates: {
4 type: min # Specifies that you want the "min" value
5 field: examsCompleted # Specifies the field for the aggregate value
6 name: "minimumExams" # provides a name to reference in the response field
7 }
8 filter: { name: { wildcard: "Rene*" } }
9 ) {
10 aggregateItems {
11 name
12 result {
13 ... on SearchableAggregateScalarResult {
14 value
15 }
16 }
17 }
18 }
19}

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

1{
2 "data": {
3 "searchStudents": {
4 "aggregateItems": [{
5 "name": "minimumExams",
6 "result": {
7 "value": 7
8 }
9 }]
10 }
11 }
12}

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

1query MyQuery {
2 searchStudents(
3 aggregates: {
4 type: max # Specifies that you want the "max" value
5 field: examsCompleted # Specifies the field for the aggregate value
6 name: "maximumExams" # provides a name to reference in the response field
7 }
8 filter: { name: { wildcard: "Rene*" } }
9 ) {
10 aggregateItems {
11 name
12 result {
13 ... on SearchableAggregateScalarResult {
14 value
15 }
16 }
17 }
18 }
19}

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

1{
2 "data": {
3 "searchStudents": {
4 "aggregateItems": [{
5 "name": "maximumExams",
6 "result": {
7 "value": 28
8 }
9 }]
10 }
11 }
12}

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

1query MyQuery {
2 searchStudents(
3 aggregates: {
4 type: avg # Specifies that you want the "avg" value
5 field: examsCompleted # Specifies the field for the aggregate value
6 name: "averageExams" # provides a name to reference in the response field
7 }
8 filter: { name: { wildcard: "Rene*" } }
9 ) {
10 aggregateItems {
11 name
12 result {
13 ... on SearchableAggregateScalarResult {
14 value
15 }
16 }
17 }
18 }
19}

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

1{
2 "data": {
3 "searchStudents": {
4 "aggregateItems": [{
5 "name": "averageExams",
6 "result": {
7 "value": 17.3
8 }
9 }]
10 }
11 }
12}

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

1query MyQuery {
2 searchStudents(
3 aggregates: {
4 type: sum # Specifies that you want the "sum" value
5 field: examsCompleted # Specifies the field for the aggregate value
6 name: "examsSum" # provides a name to reference in the response field
7 }
8 filter: { name: { wildcard: "Rene*" } }
9 ) {
10 aggregateItems {
11 name
12 result {
13 ... on SearchableAggregateScalarResult {
14 value
15 }
16 }
17 }
18 }
19}

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

1{
2 "data": {
3 "searchStudents": {
4 "aggregateItems": [{
5 "name": "examsSum",
6 "result": {
7 "value": 392
8 }
9 }]
10 }
11 }
12}

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

1query MyQuery {
2 searchTodos(
3 aggregates: { field: description, type: terms, name: "descriptionTerms" }
4 ) {
5 aggregateItems {
6 result {
7 ... on SearchableAggregateBucketResult {
8 __typename
9 buckets {
10 doc_count
11 key
12 }
13 }
14 }
15 name
16 }
17 }
18}

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

1{
2 "data": {
3 "searchTodos": {
4 "aggregateItems": [
5 {
6 "result": {
7 "__typename": "SearchableAggregateBucketResult",
8 "buckets": [{
9 "doc_count": 2,
10 "key": "Shopping list"
11 }, {
12 "doc_count": 1,
13 "key": "Me next todo"
14 }]
15 },
16 "name": "descriptionTerms"
17 }
18 ]
19 }
20 }
21}

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.

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
1{
2 "dev": {
3 "categories": {
4 "api": {
5 "<your-api-name>": {
6 "OpenSearchInstanceType": "t2.small.elasticsearch"
7 }
8 }
9 }
10 },
11 "prod": {
12 "categories": {
13 "api": {
14 "<your-api-name>": {
15 "OpenSearchInstanceType": "t2.medium.elasticsearch"
16 }
17 }
18 }
19 }
20}
  1. Deploy your changes with amplify push

Learn more about Amazon OpenSearch Service instance types here.

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:

1# Streams data from DynamoDB to OpenSearch and exposes search capabilities.
2directive @searchable(queries: SearchableQueryMap) on OBJECT
3input SearchableQueryMap {
4 search: String
5}