Node.jsJavaScript

Gatsby-Ghost Suche: Redis und RediSearch - Teil 2

Dieser Blog Post erklärt wie man mit einen kleinen HTTP Service Redis durchsucht und mit dem Ergebnis antwortet.

Mittels Node.js werden wir uns einen kleinen Server programmieren, der auf die Such - Requests antwortet.

Grundlegendes

Wir erstellen im Verzeichnis ./gatsby-srv eine neue package.json mit dem Befehl npm create und danach installieren wir einige Node Module, die wir benötigen werden.

./gatsby-srv/
$ npm create
$ npm i winston express redredisearch redis 

Neben Winston für das Logging, node-redis und RedRediSearch als Schnittstelle für RediSearch, installieren wir auch Express für den HTTP Server.


Testdaten

Falls Sie den ersten Teil noch nicht gelesen haben oder Sie einfach gerne den Server fertig hätten bevor Sie gatsby build ausführen um den Index zu erstellen, dann habe ich hier eine index.json und sitemap.json zum Testen.


Der Server

Optionen wie zum Beispiel IP Addresse und Port, sollen über Argumente beim Start einstellbar sein. Neben dem Logging in der Shell Ausgabe soll eine Log - Datei genutzt werden. Die Redis Datenbank wird optional aus einer Datei neu erstellt damit sie auch nach einem Neustart im RAM verfügbar ist und ein HTTP Server wird die Suchanfragen beantworten.

Initialisierung

Wir laden als erstes alle Module, die wir benötigen und initialisieren unsere globalen Variablen.

./index.js
const { createLogger, format, transports, winston }  = require('winston');
const redsearch                                      = require('redredisearch');
const redis                                          = require('redis');
const express                                        = require('express');
const app                                            = express();
const server                                         = require('http').createServer(app);
const router                                         = express.Router();

const env         = process.env.NODE_ENV || 'development';
const sitemap     = new Map();

let indexFile     = process.env.npm_package_config_indexFile || './index.json';
let sitemapFile   = process.env.npm_package_config_sitemapFile || './sitemap.json';
let logFile       = process.env.npm_package_config_logFile || './gatsby-srv.log';
let httpHost      = process.env.npm_package_config_http_host || '127.0.0.1';
let httpPort      = process.env.npm_package_config_http_port || 5566;
let redisHost     = process.env.npm_package_config_redis_host || '127.0.0.1';
let redisPort     = process.env.npm_package_config_redis_port || 6379;
let redisIndex    = process.env.npm_package_config_redis_index || 'redis_index';

let help = `  index\t\tFile that stores the Redis indices | standard: \x1b[33m${indexFile}\x1b[0m`;
help += `\n  sitemap\tFile that stores the relation between indices and URL's | standard: \x1b[33m${sitemapFile}\x1b[0m`;
help += `\n  log\t\tLog file path | standard: \x1b[33m${logFile}\x1b[0m`;
help += `\n  http-host\tBound IP adress for HTTP | standard: \x1b[33m${httpHost}\x1b[0m`;
help += `\n  http-port\tBound Port for HTTP | standard: \x1b[33m${httpPort}\x1b[0m`;
help += `\n  redis-host\tRedis IP adress / hostname | standard: \x1b[33m${redisHost}\x1b[0m`;
help += `\n  redis-port\tRedis port | standard: \x1b[33m${redisPort}\x1b[0m`;
help += `\n  redis-index\tRedis index | standard: \x1b[33m${redisIndex}\x1b[0m`;
help += `\n  create-db\tGenerate new database from file\n`;

Winston

Nun erklären wir Winston wie und wo es loggen soll. Mit .replace(/(\x1b\[)(\w){2,3}/g, '') suchen wir nach allen Textfarben, die in der Kommandozeile dargestellt werden und entfernen diese. In der Log - Datei werden die nicht dargestellt und übrig würde ein \x1b[33m $ \x1b[0m bleiben.

./index.js
/* ... */

const logFileFormat = format.combine(
  format.printf(
    info =>
      `${info.timestamp} ${info.level} [${info.label}]: ${info.message.replace(/(\x1b\[)(\w){2,3}/g,'')}`
  )
);

const transport = {
  console: new transports.Console({
    format: format.combine(
      format.colorize(),
      format.printf(
        info =>
          `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
      )
    )
  }),
  file: new transports.File({ filename: logFile, format: logFileFormat })
};

const log = createLogger({
  level: env === 'production' ? 'info' : 'debug',
  format: format.combine(
    format.label({ label: 'gatsby-srv' }),
    format.timestamp({ format: 'DD.MM.YYYY HH:mm:ss' })
  ),
  transports: [
    transport.console,
    transport.file
  ]
});

Funktion zum Erstellen der Datenbank

Als erstes versuchen wir die indexFile zu laden. Danach erstellen wir den Redis Client und laden den kompletten Content in Redis ein. Die Funktion gibt ein Promise zurück damit sie trotz Callback in einer asynchronen Funktion mit await aufgerufen werden kann.

./index.js
/* ... */

async function createDB() {
  return new Promise((resolve, reject) => {
    try {      
      const db = require(indexFile);
      
      const client = redis.createClient(redisPort, redisHost);
      redsearch.setClient(client); 
        
      redsearch.createSearch(redisIndex, {}, (error, search) => {  
        if(error) {
          console.log(error);
          reject(error);
        }

        for(slug in db) {          
          search.index(db[slug], slug);
          log.info(`added "/${slug}" to redis index`);
        }
        
        resolve();
      });            
    }
    catch(error) {
      reject(error);
    }
  });
}

Start Argumente

Wir iterieren durch das Array der Argumente und weisen den ensprechenden Optionen ihren neuen Werte zu.

./index.js
/* ... */

process.argv.forEach(async (arg, index) => {
  try {
    arg = arg.split('=');
    
    const option = arg[1] ? arg[1].replace('"','') : false;
    
    switch(arg[0]) {
      case 'work-dir':
        workDir = option;
        return;
      case 'log':
        logFile = option;
        log.remove(transport.file);
        transport.file = new transports.File({ filename: logFile, format: logFileFormat });
        log.add(transport.file);
        return;
      case 'index':
        indexFile = option;
        return;
      case 'sitemap':
        sitemapFile = option;
        return;
      case 'http-host':
        httpHost = option;
        return;
      case 'http-port':
        httpPort = option;
        return;
      case 'redis-host':
        redisHost = option;
        return;
      case 'redis-port':
        redisPort = option;
        return;
      case 'redis-index':
        redisIndex = option;
        return;
      case 'create-db':
        await createDB();
        process.exit(0);    
        return;
      case 'help': 
        console.log(help);
        process.exit(0);
        return;
      default: 
        if(index > 1) {
          log.error(`Uknown argument "${arg[0]}"`);
          console.log(help);
          process.exit(0); 
        }         
    }
  }
  catch(error) {
    log.error(error.message);
  }
});

HTTP Request und Redis durchsuchen

Wir initialisieren den Redis Clienten damit wir eine Query an Redis weitergeben können, die wir von einen HTTP Request erhalten. Express antwortet mit einen JSON Dokument, das alle Ergebnisse beinhaltet, oder einen internen Serverfehler (500).

./index.js
/* ... */

const client = redis.createClient(redisPort, redisHost);
redsearch.setClient(client); 

redsearch.createSearch(redisIndex,{}, async (error, search) => {  
  router.get('/search', (req, res) => {
    search
      .query(`${req.query.q}`)
      .type('or')
      .end(function(error, ids) {
        if(error) { 
          res.status(500).send(err); 
        } 
        else {   
          const hits = {};
          
          ids.forEach((id) => {
            if(sitemap.has(id)) {
              hits[id] = sitemap.get(id);                
            }
          });  
           
          res.send(hits);
        }
      });
  });
});

app.use(router);

Laden der Sitemap und HTTP Server starten

Wir laden die Sitemap und starten den HTTP Server. Im Falle eines Fehler wird dieser ausgegeben und die Anwendung wird beendet.

./index.js
/* ... */

try {    
  const sitemapData = JSON.parse(fs.readFileSync(sitemapFile));
  
  for(prop in sitemapData) {      
    sitemap.set(prop, sitemapData[prop]);
  }
      
  server.listen(httpPort, httpHost, () => {
    log.info(`HTTP Server running \x1b[33m(http://${httpHost}:${httpPort})\x1b[0m`);
  });        
}
catch(error) {
  log.error(`(code ${error.code})\n${error.message}`);
}

imageMit dem Befehl node . create-db sollten wir nun aus den Test - Dateien die Datenbank erstellen können. Danach können wir den Server mit node . starten.

Wenn alles funktioniert, sollte die URL http://localhost/search?q=ffmpeg ein in JSON kodiertes Dokument im Browser aufrufen.


PM2 zur Prozessverwaltung

Damit der Server sich wie ein Daemon verhält und er im Fehlerfall automatisch wiedr startet, kann an ihn mittels PM2 starten.

./gatsby-srv/
$ npm i -g pm2
$ pm2 start index.js --name gatsby-srv
$ pm2 save
$ pm2 startup

Der Server und das Plugin sind nun fertig. Der abschließende Teil behandelt dann die Änderungen an der React Suchkomponente für Gatsby.


Weitere Blog Posts aus dieser Serie

  1. Gatsby-Ghost Suche: React Komponente
  2. Gatsby-Ghost Suche: Webworker
  3. Gatsby-Ghost Suche: Content indizieren
  4. Gatsby-Ghost Suche: Redis und RediSearch (Teil 1)
  5. Gatsby-Ghost Suche: Redis und RediSearch (Teil 2)
  6. Gatsby-Ghost Suche: Redis und RediSearch (Teil 3)