Recherche en texte intégral

La plupart des applications permettent aux utilisateurs de rechercher du contenu dans une application. Par exemple, vous pouvez rechercher des messages contenant un ou plusieurs mots que vous avez rédigés à propos d'un sujet spécifique.

Firestore ne prend pas en charge l'indexation native et ne recherche pas les champs de texte dans les documents. En outre, le téléchargement d'une collection complète pour rechercher des champs côté client n'est pas pratique.

La solution : Algolia

Pour activer la recherche en texte intégral de vos données Firestore, utilisez un service de recherche tiers tel que Algolia. Imaginez une application de prise de notes où chaque note est un document :

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

Vous pouvez utiliser Algolia avec Cloud Functions pour renseigner un index avec le contenu de chaque note et activer la recherche. Commencez par configurer un client Algolia à l'aide de votre ID d'application et de votre clé d'API :

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

Ensuite, ajoutez une fonction qui met à jour l'index chaque fois qu'une note est écrite :

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);
});

Une fois vos données indexées, vous pouvez utiliser l'une des intégrations d'Algolia pour iOS, Android ou le Web pour effectuer une recherche dans les données.

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);
    });

Ajout de sécurité

La solution d'origine fonctionne bien si toutes les notes peuvent être recherchées par tous les utilisateurs. Toutefois, de nombreux cas d'utilisation nécessitent de limiter les résultats de manière sécurisée. Pour ces cas d'utilisation, vous pouvez ajouter une fonction HTTP Cloud qui génère une clé d'API sécurisée, qui ajoute des filtres spécifiques à chaque requête effectuée par un utilisateur.

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

Si vous utilisez cette fonction pour fournir la clé de recherche Algolia, l'utilisateur peut uniquement rechercher les notes dont le champ author correspond exactement à son ID utilisateur.

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;
  });

Il n'est pas nécessaire d'extraire une clé de recherche pour chaque requête. Vous devez mettre en cache la clé extraite, ou le client Algolia, pour accélérer la recherche.

Limitations

La solution ci-dessus est un moyen simple d'ajouter une recherche en texte intégral à vos données Firestore. Sachez toutefois qu'Algolia n'est pas le seul moteur de recherche tiers. Envisagez d'autres solutions, telles que ElasticSearch, avant de déployer une solution en production.