OpenSearch Scripte gehen ins Codeberg Repository

Auch die OpenSearch-Scripte kommen in das Codeberg Repository, damit wir die lose Sammlung etwas unter Kontrolle bekommen. Dazu habe ich ein Migrationstool implementiert, dass die Indexerstellung automatisiert.

Einleitung

Wie schon in meinem anderen Blog-Artikel werde ich nun auch die OpenSearch-Scripte für das Erstellen der Indexe für Toots und Accounts (inklusive Index-Template) in mein Codeberg Repository integrieren.

Da werde ich zudem ein grundlegendes Refactoring vornehmen und hier beschreiben, um eine Index-Collection immer wieder von Grund auf neu zu erstellen und in einer späteren Iteration bestehende Indexe zu updaten.

Das erfordert aber noch ein bis zwei weitere Artikel mit entsprechenden Grundlagen.

Das Refactoring

Login Konfiguration

Ähnlich wie das MastodonHelper.py Script benötigen wir ein OpenSearchHelper, um aus einer Konfiguration die Zugriffsinformationen zu laden. Damit machen wir die Scripte im Codeberg-git Repository unabhängig lokaler Testumgebungen.

Da es eine massiv reduzierte Variante des MastodonHelper.py Scripts ist, werde ich das hier nicht weiter ausführen. Nur zur Erinnerung: Das Repository enthält eine oscluster-config.yaml Datei im Basisordner und hat nur die Grundkonfiguration eines Dev-Systems:

# Nodes to access.
nodes:
  default:
    host: 127.0.0.1 # your master node
    port: 9200 # port for the node

Wenn das so passt, alles gut. Wenn ihr einen anderen Host oder Port habt, dann kopiert die Datei in euer Home-Verzeichnis und ändert die Parameter. Das OpenSearchHelper.py Modul sucht dort zuerst und nimmt von dort die Parameter:

    if not os.path.isabs(config_yaml):
        # First we check the home folder (will override the standard config from repository)
        config_home=os.path.expanduser(os.path.join("~", config_yaml))
        if os.path.exists(config_home):
            config_yaml = config_home

Mit der Zeit werden wir das Modul erweitern, aber so reicht uns das zunächst.

So kann man es testen (siehe os-status.py):

import json
import oshelper.OpenSearchHelper
from opensearchpy import OpenSearch

client: OpenSearch = oshelper.OpenSearchHelper.os_open(config_yaml="oscluster-config.yaml")

indexes = client.indices.get(index="*")
print(json.dumps(indexes, indent=4, default=str))

Index Erstellung

Die bisherigen Scripte zum Erstellen der Indexe für die unterschiedlichen Dokumente (unsere Toots und die Accounts der Followings und Followers) waren alles einzelne Sammlungen, die wir immer in einer bestimmten Reihenfolge aufrufen mussten. Das ist ein fehlerträchtiger Vorgang und erfordert Wissen über die Vorgänge und ist weit entfernt von einer Automatisierung, die man sich von Scripten eigentlich erhofft. Es war aber auch nicht die Intention der letzten Artikel. Da ich aber weiter fortschreitende Techniken für OpenSearch vorstellen möchte, macht es inzwischen Sinn einige Sachen nicht immer manuell durchzuführen.

OpenSearch ist sehr befehlsorientiert, das über Schnittstellen für Entwickler. Es gibt wenige UI Tools, die dabei unterstützen und in unserem Python-Umfeld ist uns das auch egal. Wir brauchen also ein Tool, dass uns ein generelles Setup unserer Indexe erstellt, Schritt für Schritt und möglichst ohne den Sourcecode deutlich verändern zu müssen. Mir schwebt da eine Trennung zwischen OpenSearch API Aufrufen und Request-Objekten vor.

Was haben wir bisher gehabt?

  1. Index Templates erzeugen
  2. Index erstellen
    • Index Konfiguration
    • Index Mapping

Dazu noch

  • Index löschen

Das sind alles unterschiedliche API Endpunkte unterschiedlicher APIs (Index Template API, Index API). Die müssen wir unter einen Hut bringen, möglichst in korrekte Reihenfolge.

Wenn wir unsere Request-Objekte (Payload) der API Endpunkte also vom Code trennen (und gesondert abspeichern), müssen wir später wissen, zu was für einem Aufruf die gehören. Als Speicherformat nehme ich natürlich JSON, das bietet sich an. Ich kann damit auch gleich in dem JSON Objekt abspeichern, wohin der Request gehört. Diese “Metainformation” sollte aber nicht mit dem Payload des Requests interferieren. Also trenne ich die Information ebenfalls in dem Speicher-Objekt.

Beispiel:

{
    "run": [
        {"call": "create template", "template_name": "standard", "body": "create_standard"}
    ],
    "payloads": {
        "create_standard": {
            "index_patterns" : ["*"],
            "priority" : 1,
            "template": {
                "settings" : {
                        #...
                }
            }
        }
    }
}

Das run Attribut ist unsere Liste an Befehlen. Mit call definieren wir unseren API-Aufruf und weitere Attribute sind “well-known” Parameter für die Funktion, die unser Payload bekommt. Das Payload ist erstmal nur das pure Request Body Objekt, das wir 1:1 an die OpenSearch Python-Library übergeben.

Wir benötigen also ein Script, dass dieses JSON Objekt übergeben bekommt, run ausliest und je nach Inhalt von call eine Funktion mit Parametern und Body aufruft. Wenn wir davon ausgehen, dass wir mehrere API Endpunkt-Aufrufe haben, gibt es mehrere JSON Dateien. Dazu eine definierte Reihenfolge.

Die obige Struktur wirkt erstmal unnötig komplex. Ich erkläre das später.

Am einfachsten ist, wir speichern die JSON Objekte mit Dateinamen, die man sortieren kann und zugleich noch direkt am Namen erkennen kann, was sie tun. Beispiele (und irgendjemand wird nun sehen, woher her ich die “Idee” habe):

V01.000__CreateIndexTemplate.json
V01.001__CreateIndexToots.json
V01.002__CreateIndexFollowers.json
V01.003__CreateIndexFollowings.json
V01.004__CreateIndexUnFollowers.json
V01.005__CreateIndexUnFollowings.json

Das sieht schwer nach Flyway aus - und ja - das können wir von der Idee genau so übernehmen. Allerdings trivialer und für unsere Zwecke angemessen.

Wenn aber oben im JSON sieht, dass wir mit Listen von Befehlen arbeiten, kann man das Erstellen der Accounts schon zusammenfassen:

V01.000__CreateIndexTemplate.json
V01.001__CreateIndexToots.json
V01.002__CreateIndexAccounts.json

…wir sind ja faul.

Es gibt da noch Punkte, die wir jetzt noch ausklammern werden (aber später hinzufügen):

  1. Updates von Indexen mit Daten
  2. Erstellen von beliebigen Index-Collections in eigenem Namensraum
    • Damit wir mit verschiedenen Versionen und Testständen arbeiten können

Ihr seht, wir haben einiges vor.

Migration.py

Das neue Modul-Script Migration.py soll die technische Automatisierung übernehmen, was wir bisher immer als einzelne Python Scripts aufgerufen haben. Die API Endpunkt-Aufrufe werden in einem Ordner gesammelt.

Das Script umfasst weniger als 300 Zeilen und viel ist davon Dokumentation und Validierungen, ob alle Daten einigermaßen passen.

Ein sehr grobes Aktivitätsdiagramm einer Migration:

Migration activity-flow

Es gibt eine Menge Hilfsfunktionen in dem Script, auf die ich nur mal als Hinweis darauf eingehen kann:

  • list_migration_files
    • Gibt eine sortierte Liste an Dateien eines Ordners zurück. Es werden nur Dateien mit einem bestimmten Muster gelistet und die Sortierung erfolgt nach der Versionsnummer. Es sind nur Versionnummern mit Haupt und Unterversionen zulässig.
  • _load_migration
    • Lädt ein Migration File als JSON in ein dict und reichert das Dictionary per _inject_migration_meta mit Zusatzinformationen an. Das wird später genutzt, um auch etwas in den migration_history Index zu schreiben.
  • _resolve_migration_runners
    • Sieht sehr kompliziert aus. Aber es ordnet nur dem body Attribut einer Befehlszeile den passenden payload zu. Damit können wir mehrere Befehle mit immer dem selben Payload durchführen. Schaut dazu das V01.003__CreateAccountsIndexes.json an.
  • _run_create_index und _run_create_index_template
    • Hier passiert der Aufruf der OpenSearch-API Endpunkte. Diese beiden Methoden werden als Callback Functions in der Map _CALLBACK_RUNNERS gespeichert und _run_migration_runner ist die Methode, die anhand der call-Attribute in den JSON Dateien entscheidet, welcher API Aufruf erfolgt. Also eine reine Übersetzung von Befehl im Script zur Function im Python-Script.
  • migration_history_prepare
    • Das ist die simple Version einer Migration, die nur den migration_history Index anlegt, damit wir später verfolgen können, welche Scripte bereits migriert wurden (und in Zukunft ignoriert werden können)
  • migration_history_delete
    • Löscht den migration_history Index (zum Testen)
  • _check_migrated
    • Wird von der migration Funktion benötigt, um zu schauen, ob eine Version schon migriert wurde. Das ist ein schönes Beispiel einer exakten Filter-Suche auf einen Index.
  • _create_history
    • Bereitet das History Dokument mit allen Metadaten vor. Danach wird erst versucht das Script auszuführen.
  • _update_history_success
    • Wenn ein Script einer Version erfolgreich war wird das History Dokument mit den Daten versorgt, damit das Migrationsscript in Zukunft nicht mehr ausgeführt wird (success=True und ein paar weitere Werte)
  • _update_history_error
    • Gab es ein Fehler bei der Script-Ausführung, fügen wir hier den Fehler an das message Attribut an. Als klassisches “get + alter + index”, also Dokument lesen, dann die Attribute ändern, dann das geänderte Dokument mit der selben _id wieder speichern.
  • migration
    • Last, but not least, der Befehl zur vollständigen Migration.

Der Aufruf ist einfach (os-migration.py):

import os.path
import oshelper.OpenSearchHelper
from opensearchpy import OpenSearch
from oshelper import Migration


client: OpenSearch = oshelper.OpenSearchHelper.os_open(config_yaml="oscluster-config.yaml")

path = os.path.abspath("./os/migration/")

Migration.migration(client, path)

Über den oshelper.OpenSearchHelper verbinden wir uns an OpenSearch. Im path finden sich die Migrationsscripte. Das zusammen übergeben wir an Migration.migration

Wenn ihr mal alle eure Indexe löscht, um eine volle Migration zu testen (ja, da gehen die alten Daten verloren), dann bekommt man diese Ausgabe:

Run V01.001__CreateTemplate.json
  success
Run V01.002__CreateTootsIndex.json
  success
Run V01.003__CreateAccountsIndexes.json
  success

Wenn ihr den toots-Index und die vier Indexe für die Account nicht wegwerfen wollt, dann gibt es folgende Ausgabe:

Run V01.001__CreateTemplate.json
  success
Run V01.002__CreateTootsIndex.json
  error

Das V01.001__CreateTemplate.json ist erfolgreich, weil die API create oder update macht. Das Template wird also neu angelegt oder aktualisiert.

Das Erstellen des toots Index wird aber mit Fehler quittiert. Danach bricht die Migration ab. Alle Migrationsschritte sind strikt aufeinanderfolgend.

Ruft ihr jetzt nochmal das Script auf, gibt es nur noch diese Ausgabe:

Run V01.002__CreateTootsIndex.json
  error

Die Version 01.001 wurde nicht mehr aufgrufen, weil in der Historie vermerkt wurde, dass das schon mal erfolgreich war. Stoisch versuchte aber die migration-Funktion es nochmal mit V01.002__CreateTootsIndex.json zu probieren - vergeblich, damit ein sofortiger Abbruch.

Die message_history wird so abgefragt:

http://localhost:9200/migration_history/_search

Achtung, das Ergebnis ist nicht sortiert! Je nachdem, wie häufig man Fehler auf einer Version bekommen hat, desto mehr steht in dem message Attribut.