Node.jsReactJavaScript

Gatsby-Ghost Suche: Content indizieren

Der Ghost Content API mangelt es derzeit noch an einigen wichtigen Features um effizient Daten abzufragen. Man ist zum Beispiel gezwungen die komplette Tabelle an Datensätze zu laden, weil Out-of-the-box kein LIKE bzw. eine teilweise Übereinstimmung unterstützt wird.

Um den Datenbank Server zu entlasten, könnte man beim Build - Prozess von Gatsby einen Index anlegen und diesen durchsuchen. Das hat gleich mehrere Vorteile. Zum einem wird nur ein einzelner Request an den Server gestellt um die zu durchsuchenden Daten zu laden, es kann vorab definiert werden in welcher Form die Datensätze vorliegen sollen und der Service Worker vom gatsby-plugin-offline kann den Index ohne weiteres lokal speichern und auch precachen.

Setup

Wir wechseln in den Ordner Plugin und erstellen ein neues lokales Plugin.

$ cd plugins
$ gatsby new gatsby-plugin-search https://github.com/gatsbyjs/gatsby-starter-plugin

Module und Funktionen zum bereinigen des Contents

Wir müssen nur die gatsby-node.js unseres Plugins bearbeiten. Zum Anfang kümmern wir uns um den Import von benötigten Node.js Modulen und fügen eine Funktion zum entfernen von Whitespaces und eine die Zeilenumbrüche und Tabulatoren entfernt.

plugins/gatsby-plugin-search/gatsby-node.js
const fs = require("fs");
const path = require("path");
const gatsby = require('gatsby');

function trimChar(string, character) {
  const first = [...string].findIndex(char => char !== character);
  const last = [...string].reverse().findIndex(char => char !== character);
  return string.substring(first, string.length - last);
}

function remLBaTabs(str) {
    if(str !== undefined)
        return (
            str.split('\n').join()
                .split('\t').join()
                .split('\\"').join('"')
        );
}

Die Funktion onPostBootstrap

Wir definieren einige Variablen und prüfen ob die benötigten Argumente in Ordnung sind.

plugins/gatsby-plugin-search/gatsby-node.js
exports.onPostBootstrap = async (_ref, pluginOptions) => {
	const page = _ref.page;
	const actions = _ref.actions;
	const createPage = actions.createPage;
	const {getNodesByType, reporter} = _ref;
	const start = Date.now();

	let indexed = [];
	let options = {
		file: '/public/indexed.json'
	};	

	options = {...options, ...pluginOptions};

	if(options.nodes == undefined && options.index == undefined) {
		reporter.warn('No data to index');
		return;
	}
    
    /* ... */
}

Daten für den Index aufbereiten

Der Content wird hier für den Index in das passende Format gebracht und bereinigt.

plugins/gatsby-plugin-search/gatsby-node.js
exports.onPostBootstrap = async (_ref, pluginOptions) => {
    /* ... */
    
    options.nodes.forEach((node) => {
		getNodesByType(node.node_type)
		.forEach((n) => {
			const obj = new Object();						
			obj.id = n[node.id];
			obj.title = n[node.title_field];	
			obj.search = new Object();
            obj.slug = n[node.slug_field];

			node.fields.forEach((val) => {
			 	obj.search[val] = remLBaTabs(trimChar(n[val], ' '));
			});

			indexed.push(obj);
		});
	});
    
    /* ... */
}

Die Index-Datei speichern

Nun müssen die Daten nur noch in der Index-Datei gespeichert werden.

plugins/gatsby-plugin-search/gatsby-node.js
exports.onPostBootstrap = async (_ref, pluginOptions) => {
    /* ... */
    
    fs.writeFile(
        path.join(__dirname,'./../../', options.file),
        JSON.stringify(indexed), 
        (err) => {		
		fs.stat(path.join(__dirname,'./../../', options.file), 
        		(err, fstat) => {
                   		if(err) {
                        	reporter.error(`Error: ${err.message}`); 
                        	return;
                    	}
			const duration = ((Date.now() - start) / 1000).toFixed(2);
			reporter.success(`search index saved to \"${options.file}\" (${(fstat.size / 1024).toFixed(2)} KB) - ${duration}s`);				
		});

		if(err) reporter.error(`Error: ${err.message}`); 
	});
    
    /* ... */
}

Das Plugin laden

Nun muss der Plugin Name plus benötigter Optionen in der Datei gatsby-config.js im plugins Object Array hinzugefügt werden. Da plaintext indiziert wird, können wir den kompletten Inhalt durchsuchen. Wenn die Index - Datei zu groß werden sollte, kann man darauf auch verzichten.

gatsby-config.js
plugins: [
    /**
    *  Content Plugins
    */
    {
        resolve: require.resolve('./plugins/gatsby-plugin-search'),
        options: {
            nodes: [
                {
                    'node_type': 'GhostPage',
                    'slug_field': 'slug',
                    'title_field': 'title',
                    'id': 'id',
                    'fields': [
                        'plaintext',
                        'title',
                        'excerpt'
                    ]
                },
                {
                    'node_type': 'GhostPost',
                    'slug_field': 'slug',
                    'title_field': 'title',
                    'id': 'id',
                    'fields': [
                        'plaintext',
                        'title',
                        'excerpt'
                    ]
                }
            ]
        }
    },
    
    /* ... */

Webworker

Im Webworker Script passen wir die search Funktion wie folgend an.

src/utils/search.worker.js
export async function search(query, max) {
    const response = await fetch('/indexed.json');
    const data = await response.json();
    
    const results =
                data.filter((obj) => {
                    for(let key in obj.search) {                        
                        if(obj.search[key].toLowerCase().includes(query))
                            return true;
                    }
                })
                .slice(0, max); 

    results.forEach((val, index, arr) => {
        delete arr[index].search;
    });

	return results;
}

Die Suche komplett vom Client ausführen zulassen mag in diesen Beispiel keine Probleme bereiten. Jedoch ist das eher die Ausnahme und mit zunehmenden Datensätzen erhöht sich natürlich auch die Datenmenge, die initial geladen werden muss. Den Suchprozess auf einen Server zu verlagern macht also durchaus Sinn und im ersten Teil von Gatsby-Ghost Suche: Redis und RediSearch möchte ich auf eine mögliche Lösung eingehen.


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)