Full Text Search

Most apps allow users to search app content. For example, you may want to search for posts containing a certain word or notes you've written about a specific topic.

Cloud Firestore doesn't support native indexing or search for text fields in documents. Additionally, downloading an entire collection to search for fields client-side isn't practical.

Solution: Algolia

To enable full text search of your Cloud Firestore data, use a third-party search service like Algolia. Consider a note-taking app where each note is a document:

// /notes/${ID}
{
  owner: "{UID}", // Firebase Authentication's User ID of note owner
  text: "This is my first note!"
}

You can use Algolia with Cloud Functions to populate an index with the contents of each note and enable search. First, configure an Algolia client using your App ID and API key:

Node.js

// Initialize Algolia, requires installing Algolia dependencies:
// https://www.algolia.com/doc/api-client/javascript/getting-started/#install
//
// App ID and API Key are stored in functions config variables
const ALGOLIA_ID = functions.config().algolia.app_id;
const ALGOLIA_ADMIN_KEY = functions.config().algolia.api_key;

const ALGOLIA_INDEX_NAME = "notes";
const client = algoliasearch(ALGOLIA_ID, ALGOLIA_ADMIN_KEY);

Next, add a function that updates the index each time a note is written:

Node.js

// Update the search index every time a blog post is written.
exports.onNoteCreated = firestore.document("notes/{noteId}").onCreate(event => {
  // Get the note document
  const note = event.data.data();

  // Add an "objectID" field which Algolia requires
  note.objectID = event.params.postId;

  // Write to the algolia index
  const index = client.initIndex(ALGOLIA_INDEX_NAME);
  return index.saveObject(note);
});

Once your data is indexed, you can use any of Algolia's integrations for iOS, Android, or Web to search through the data.

Web

const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_KEY);
const index = client.initIndex("notes");

// Search query
const query = "Some text";

// Perform an Algolia search:
// https://www.algolia.com/doc/api-reference/api-methods/search/
index
  .search({
    query
  })
  .then(responses => {
    // Response from Algolia:
    // https://www.algolia.com/doc/api-reference/api-methods/search/#response-format
    console.log(responses.hits);
  });

Adding Security

The original solution works well if all notes are searchable by all users. However, many use cases require securely limiting results. For these use cases, you can add an HTTP Cloud Function that generates a Secured API Key, which adds specific filters to every query performed by a user.

Node.js

// This complex HTTP function will be created as an ExpressJS app:
// https://expressjs.com/en/4x/api.html
const app = require("express")();

// We'll enable CORS support to allow the function to be invoked
// from our app client-side.
app.use(require("cors")({ origin: true }));

// Then we'll also use a special "getFirebaseUser" middleware which
// verifies the Authorization header and adds a `user` field to the
// incoming request:
// https://gist.github.com/abehaskins/832d6f8665454d0cd99ef08c229afb42
app.use(getFirebaseUser);

// Add a route handler to the app to generate the secured key 
app.get("/", (req, res) => {
  // Create the params object as described in the Algolia documentation:
  // https://www.algolia.com/doc/guides/security/api-keys/#generating-api-keys
  const params = {
    // This filter ensures that only documents where author == user_id will be readable
    filters: `author:${req.user.user_id}`,
    // We also proxy the user_id as a unique token for this key.
    userToken: req.user.user_id
  };

  // Call the Algolia API to generate a unique key based on our search key
  const key = client.generateSecuredApiKey(ALGOLIA_SEARCH_KEY, params);

  // Then return this key as {key: "...key"}
  res.json({ key });
});

// Finally, pass our ExpressJS app to Cloud Functions as a function
// called "getSearchKey";
exports.getSearchKey = functions.https.onRequest(app);

If you use this function to provide the Algolia search key, the user can only search for notes where the author field is exactly equal to their user ID.

Web

const projectID = "YOUR_PROJECT_ID";

// Search query
const query = "Some text";

// Use Firebase Authentication to request the underlying token
firebase.auth().currentUser.getIdToken()
  .then(token => {
    // The token is then passed to our getSearchKey Cloud Function
    return fetch(`https://us-central1-${projectID}.cloudfunctions.net/getSearchKey/`, {
        headers: { Authorization: `Bearer ${token}` }
    });
  })
  .then(r => {
    // The Fetch API returns a stream, which we convert into a JSON object.
    return r.json();
  })
  .then(data => {
    // Data will contain the restricted key in the `key` field.
    this.algolia.client = algoliasearch(ALGOLIA_APP_ID, data.key);
    this.algolia.index = this.algolia.client.initIndex("notes");

    // Perform the search as usual.
    return index.search({query});
  })
  .then(responses => {
    console.log(responses.hits);
  });

It's not necessary to fetch a search key for every query. You should cache the fetched key, or the Algolia client, to speed up searching.

Limitations

The solution shown above is a simple way to add full-text search to your Cloud Firestore data. However, be aware that Algolia is not the only third-party search provider. Consider alternatives such as ElasticSearch before deploying any solution to production.

Send feedback about...

Need help? Visit our support page.