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.

To enable full text search of your Cloud Firestore data, use a dedicated third-party search service. These services provide advanced indexing and search capabilities far beyond what any simple database query can offer.

Before continuing, research then choose one of the search providers below:

Solution: External search service

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 Elastic with Cloud Functions to populate an index with the contents of each note and enable search. First, configure an Elastic client using your Elastic instance ID, username, and password:

Node.js

const { Client } = require("@elastic/elasticsearch");

// Initialize Elastic, requires installing Elastic dependencies:
// https://github.com/elastic/elasticsearch-js
//
// ID, username, and password are stored in functions config variables
const ELASTIC_ID = functions.config().elastic.id;
const ELASTIC_USERNAME = functions.config().elastic.username;
const ELASTIC_PASSWORD = functions.config().elastic.password;

const client = new Client({
  cloud: {
    id: ELASTIC_ID,
    username: ELASTIC_USERNAME,
    password: ELASTIC_PASSWORD,
  }
});

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 = functions.firestore.document('notes/{noteId}').onCreate(async (snap, context) => {
  // Get the note document
  const note = snap.data();

  // Use the 'nodeId' path segment as the identifier for Elastic
  const id = context.params.noteId;

  // Write to the Elastic index
  client.index({
    index: "notes",
    id,
    body: note,
  });
});

Once your data is indexed, you use a callable Cloud Function to search the index:

Node.js

exports.searchNotes = functions.https.onCall(async (data, context) => {
  const query = data.query;

  // Search for any notes where the text field contains the query text.
  // For more search examples see:
  // https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/search_examples.html
  const searchRes = await client.search({
    index: "notes",
    body: {
      query: {
        query_string: {
          query: `*${query}*`,
          fields: [
            "text"
          ]
        }
      }
    }
  });

  // Each entry will have the following properties:
  //   _score: a score for how well the item matches the search
  //   _source: the original item data
  const hits = searchRes.body.hits.hits;

  const notes = hits.map(h => h["_source"]);
  return {
    notes: notes
  };
});

Then you can call the search function using the Firebase SDK:

Web

const searchNotes = firebase.functions().httpsCallable('searchNotes');
searchNotes({ query: query })
  .then((result) => {
    const notes = result.data.notes;
    // ...
  });

Adding Security

The original solution works well if all notes are searchable by all users. However, many use cases require securely limiting results.

In this case the simplest security measure would be to check the uid of the user that calls the Cloud Function and then limit the search to notes where the owner field matches the user's ID.

Elastic also offers many authenticationand authorization strategies that you can use to provide role-based access to your search index. For more information see Demystifying authentication and authorization in Elasticsearch

Limitations

  • Pricing: Elastic Cloud offers many service tiers, make sure to visit the pricing page and choose the option that best fits your needs.