Question about Cells in regards to Algolia search functionality

I am trying to build Algolia search functionality into an existing project. The first step is to populate the Algolia indices with the data from the project. I am trying to do this via a GraphQL query that transmits the queried data to Algolia upon running yarn rw build.

I have done this before using GatsbyJS by putting the query into a page titled Search… I tried the same thing with Redwood and when that did not work I am thinking perhaps I need to use a Cell instead…

The GraphQL docs state that if you want to use useQuery (which I think I do :rofl:) it is best to use a cell. So, I ran yarn rw g cell Search to build a Search Cell and when I open the file I see that it built import type { FindSearchQuery } from 'types/graphql' at the top. However, the linter does not like it and the error message reads:

import FindSearchQuery
Module '"types/graphql"' has no exported member 'FindSearchQuery'.

When I run yarn rw dev I get this error:

Generating TypeScript definitions and GraphQL schemas...
gen | GraphQL Document Validation failed with 2 errors;
gen |   Error 0: GraphQLDocumentError: Unknown type "Int".
gen |     at /Users/isaactait/Desktop/Web_Dev/cadhub/app/web/src/components/SearchCell
gen | /SearchCell.tsx:2:30
gen |
gen | Error 1: GraphQLDocumentError: Cannot query field "search" on type "Query".
gen |     at /Users/isaactait/Desktop/Web_Dev/cadhub/app/web/src/components/SearchCell
gen | /SearchCell.tsx:3:5
gen | GraphQL Document Validation failed with 2 errors;
gen |   Error 0: GraphQLDocumentError: Unknown type "Int".
gen |     at /Users/isaactait/Desktop/Web_Dev/cadhub/app/web/src/components/SearchCell
gen | /SearchCell.tsx:2:30
gen |
gen | Error 1: GraphQLDocumentError: Cannot query field "search" on type "Query".
gen |     at /Users/isaactait/Desktop/Web_Dev/cadhub/app/web/src/components/SearchCell
gen | /SearchCell.tsx:3:5
gen |
gen | Error: Could not generate GraphQL type definitions (web)

So, my first question. What is going on here with all the errors right off the bat?

My second question is do I need a plural or singular cell for a search query?

My last question is (and perhaps the most pertinent one) does this even make sense? Am I on the right track? I am learning a lot of new things all at once (RW heart and soul, GraphQL, Apollo, Algolia) and it is entirely possible that I am not putting them together in the correct way to build Algolia search into the project.

Any help would be greatly appreciated! Thank you :smiling_face_with_three_hearts:

Sounds like you’re on the right track! To populate Algolia you can do something simple like a script that you call periodically to put new results up:

yarn rw generate script populateSearch

You’ll get a file at scripts/populateSearch.js and then you can run that script with:

yarn rw exec populateSearch

Or if you always want to run it at the same time as the build:

yarn rw exec populateSearch && yarn rw build

You may be able to hook up some custom magic with webpack to have it run that script with just the build command but that’s beyond my skills!

When you want to actually integrate search into your site, you’ll do something like the following:

You created a cell with a query at the top there. Now you need an SDL file to describe the GraphQL types/fields that will be available, and then a Service to actually define the resolver for the type(s) in the SDL.

We have a generator for SDLs/Services but it’s currently limited to only being able to automatically generate them if you have a Prisma model with the same name. So you’ll have to create one manually for now. Here’s a basic shell to get you started:

// api/src/graphql/search.sdl.js

// You may get more data back from Algolia than just "text" but this
// is a good start:

export const schema = gql`
  type Result {
    text: String!
  }

  type Query {
    search(input: String!): [Result!]! @skipAuth
  }
`

And then the service to actually return results:

// api/src/services/search/search.js

export const search = ({ input }) => {
  // algolia search here, should return an array of
  // objects that have the same properties as the Result 
  // type in the SDL. For example:

  return [{ text: 'Redwood is the best' }, { text: 'I love Redwood' }]
}

Which would make your cell’s QUERY looks something like:

// web/src/components/SearchCell.js

export const QUERY = gql`
  query SearchQuery($input: String!) {
    search(input: $input) {
      text
    }
  }
`

But, where does input come from? When you include the Cell component in its parent (a Page perhaps?) you just include input as a prop and the Cell will pick it up automatically and include it in the QUERY for you (cells call useQuery automatically):

// web/src/pages/SearchPage/SearchPage.js

import SearchCell from 'src/components/SearchCell'

const SearchPage = () => {

  return (
    // just need an <input> somewhere that accepts the search text somewhere,
    // and let's assume the value in that <input> is saved in a `searchText`
    // variable, then you can just do:

    { searchText && <SearchCell input={searchText} /> }
  )
}

If there’s no search text then nothing is rendered. While the search is being performed, the Cell will render the <Loading> component, if there are no results, the Cell will render the <Empty> component, and if there are results it’ll render the <Success> component! Pretty cool, huh?

2 Likes

I concur that doing this via the redwood exec script is best — but you may want to simply look into using Prisma to query or to import a service to fetch data instead of making a GraphQL query.

Then send that to the Algolia index.

That said, I think @Tobbe recently made some GraphQL query from a script and could share his solution.

2 Likes

:exploding_head: - so cool!

Thank you very much for this detailed response @rob Suddenly this is looking a lot less daunting :smile:

1 Like

I had not thought of Prisma @dthyresson Thank you for the suggestion. I am interested to see what @Tobbe came up with :exclamation:

I’ll have to copy/paste select parts of my code. Hopefully you can piece it back together to something that’ll work for you (if you want to go down this route)

// $api is special syntax available in RW scripts
import { handler } from '$api/src/functions/graphql'

const body = {
  operationName,
  query,
  variables,
}

const handlerResult = await handler(buildApiEvent(body), buildContext())

return JSON.parse(handlerResult?.body || '{}')

function buildApiEvent(body) {
  return {
    body: JSON.stringify(body),
    headers: {
      origin: 'http://localhost:8910',
      accept: '*/*',
      host: 'localhost:8910',
    },
    multiValueHeaders: null,
    httpMethod: 'POST',
    isBase64Encoded: false,
    path: '/graphql',
    pathParameters: null,
    queryStringParameters: {},
    multiValueQueryStringParameters: null,
    stageVariables: null,
    resource: '',
    requestContext: {
      requestId: 'req-3',
      identity: {
        sourceIp: '::1',
        accessKey: null,
        accountId: null,
        apiKey: null,
        apiKeyId: null,
        caller: null,
        clientCert: null,
        cognitoAuthenticationProvider: null,
        cognitoAuthenticationType: null,
        cognitoIdentityId: null,
        cognitoIdentityPoolId: null,
        principalOrgId: null,
        user: null,
        userAgent: null,
        userArn: null,
      },
      authorizer: {},
      protocol: 'http',
      httpMethod: 'POST',
      path: '/graphql',
      stage: '',
      requestTimeEpoch: 0,
      resourceId: '',
      resourcePath: '',
      accountId: '',
      apiId: '',
    },
  }
}

function buildContext() {
  return {
    callbackWaitsForEmptyEventLoop: false,
    functionName: '',
    functionVersion: '',
    invokedFunctionArn: '',
    memoryLimitInMB: '',
    awsRequestId: '',
    logGroupName: '',
    logStreamName: '',
    getRemainingTimeInMillis: () => 100,
    done: () => {},
    fail: () => {},
    succeed: () => {},
  }
}
2 Likes