Volltextsuche

In den meisten Apps können Nutzer nach App-Inhalten suchen. Sie können beispielsweise nach Beiträgen suchen, die ein bestimmtes Wort oder Notizen enthalten, die Sie zu einem bestimmten Thema geschrieben haben.

Firestore unterstützt die native Indexierung oder die Suche nach Textfeldern in Dokumenten nicht. Es ist außerdem unpraktikabel, eine ganze Sammlung herunterzuladen, um clientseitig nach Feldern zu suchen.

Lösung: Algolia

Wenn Sie Volltextsuchen in Ihren Firestore-Daten ermöglichen möchten, verwenden Sie einen Drittanbieter-Suchdienst wie Algolia. Nehmen wir als Beispiel eine Notizanwendung, bei der jede Notiz ein Dokument ist:

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

Sie können über Cloud Functions mit Algolia einen Index mit dem Inhalt aller Notizen füllen und so die Suche möglich machen. Konfigurieren Sie als Erstes einen Algolia-Client mit Ihrer App-ID und Ihrem API-Schlüssel:

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_SEARCH_KEY = functions.config().algolia.search_key;

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

Fügen Sie als Nächstes eine Funktion ein, die den Index jedes Mal aktualisiert, wenn eine Notiz geschrieben wird:

Node.js

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

  // Add an 'objectID' field which Algolia requires
  note.objectID = context.params.noteId;

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

Sobald Ihre Daten indexiert sind, können Sie eine beliebige Integration von Algolia für iOS, Android oder das Web verwenden, um in den Daten zu suchen.

Web

  var client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_KEY);
  var index = client.initIndex('notes');

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

Sicherheit erhöhen

Die ursprüngliche Lösung funktioniert gut, wenn alle Notizen von allen Nutzern durchsucht werden können. Viele Anwendungsfälle erfordern jedoch eine sichere Einschränkung der Ergebnisse. Für diese Anwendungsfälle können Sie sich einer HTTP-Cloud Functions-Funktion bedienen, die einen gesicherten API-Schlüssel generiert, der auf jede Suchanfrage eines Nutzers bestimmte Filter anwendet.

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/abeisgoat/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);

Wenn Sie den Algolia-Suchschlüssel mit dieser Funktion bereitstellen, kann der Nutzer nur nach Notizen suchen, deren author-Feld genau mit seiner User-ID übereinstimmt.

Web

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

    // Perform the search as usual.
    return index.search({query});
  })
  .then(function(responses) {
    // Finally, use the search 'hits' returned from Algolia.
    return responses.hits;
  });

Der Suchschlüssel muss nicht für jede Abfrage neu abgerufen werden. Speichern Sie den abgerufenen Schlüssel oder den Algolia-Client im Cache, um die Suche zu beschleunigen.

Einschränkungen

Mit der oben gezeigten Lösung können Sie Ihre Firestore-Daten ganz einfach um eine Volltextsuche erweitern. Beachten Sie jedoch, dass Algolia nicht der einzige Drittanbieter für Suchen ist. Ziehen Sie auch Alternativen wie ElasticSearch in Betracht, bevor Sie eine Lösung in der Produktion bereitstellen.