Blog

Elasticsearch in JavaScript richtig anwenden, Teil II

Erfahren Sie mehr über Best Practices für die Produktion und darüber, wie Sie den Elasticsearch Node.js-Client in serverlosen Umgebungen ausführen können, um Programmierfehler zu vermeiden.

Dies ist der zweite Teil unserer Elasticsearch-in-JavaScript-Serie. Im ersten Teil haben wir gelernt, wie wir unsere Umgebung richtig einrichten, den Node.js-Client konfigurieren, Daten indizieren und suchen. Im zweiten Teil lernen wir, wie man Best Practices für die Produktion implementiert und den Elasticsearch Node.js -Client in serverlosen Umgebungen ausführt.

Wir werden Folgendes überprüfen:

Den Quellcode mit den Beispielen finden Sie hier.

Best Practices für die Produktion

Fehlerbehandlung in Elasticsearch

Ein nützliches Feature des Elasticsearch-Clients in Node.js ist, dass er Objekte für die möglichen Fehler in Elasticsearch bereitstellt, sodass man diese auf verschiedene Weise validieren und behandeln kann.

Um sie alle zu sehen, führen Sie folgenden Befehl aus: 

const { errors } = require('@elastic/elasticsearch')
console.log(errors)

Kehren wir zum Suchbeispiel zurück und behandeln wir einige der möglichen Fehler:

app.get("/search/lexic", async (req, res) => {
 ....
  } catch (error) {
    if (error instanceof errors.ResponseError) {
      let errorMessage =
        "Response error!, query malformed or server down, contact the administrator!";

      if (error.body.error.type === "parsing_exception") {
        errorMessage = "Query malformed, make sure mappings are set correctly";
      }

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error: errorMessage,
      });
    }

    res.status(500).json({
      success: false,
      results: null,
      error: error.message,
    });
  }
});

ResponseError Dies tritt insbesondere dann auf, wenn die Antwort 4xx oder 5xx lautet, was bedeutet, dass die Anfrage fehlerhaft ist oder der Server nicht verfügbar ist.

Wir können diese Art von Fehler testen, indem wir fehlerhafte Abfragen generieren, beispielsweise den Versuch, eine Termabfrage für ein Textfeld durchzuführen:

Standardfehler:

 {
    "success": false,
    "results": null,
    "error": "parsing_exception\n\tRoot causes:\n\t\tparsing_exception: [terms] query does not support [visit_details]"
}

Benutzerdefinierter Fehler:

{
    "erroStatus": 400,
    "success": false,
    "results": null,
    "error": "Response error!, query malformed or server down; contact the administrator!"
}

Wir können auch jeden Fehlertyp auf eine bestimmte Weise erfassen und behandeln. Zum Beispiel können wir in einem TimeoutError eine Wiederholungslogik hinzufügen.

app.get("/search/semantic", async (req, res) => {
    try {
  ...
  } catch (error) {
    if (error instanceof errors.TimeoutError) {


     // Retry logic...

      res.status(error.meta.statusCode).json({
        erroStatus: error.meta.statusCode,
        success: false,
        results: null,
        error:
          "The request took more than 10s after 3 retries. Try again later.",
      });
    }
  }
});

Tests

Tests sind entscheidend, um die Stabilität der App zu gewährleisten. Um den Code auf eine von Elasticsearch isolierte Weise zu testen, können wir die Bibliothek elasticsearch-js-mock beim Erstellen unseres Clusters verwenden.

Mithilfe dieser Bibliothek können wir einen Client instanziieren, der dem realen Client sehr ähnlich ist, aber auf unsere Konfiguration reagiert, indem wir lediglich die HTTP-Schicht des Clients durch eine simulierte Schicht ersetzen, während der Rest mit dem Original übereinstimmt.

Wir werden die Mock-Bibliothek und AVA für automatisierte Tests installieren.

npm install @elastic/elasticsearch-mock

npm install --save-dev ava

Wir werden die Datei package.json so konfigurieren, dass die Tests ausgeführt werden. Stellen Sie sicher, dass es so aussieht:

"type": "module",
	"scripts": {
		"test": "ava"
	},
	"devDependencies": {
		"ava": "^5.0.0"
	}

Erstellen wir nun eine test.js -Datei und installieren wir unseren Mock-Client:

const { Client } = require('@elastic/elasticsearch')
const Mock = require('@elastic/elasticsearch-mock')

const mock = new Mock()
const client = new Client({
  node: 'http://localhost:9200',
  Connection: mock.getConnection()
})

Fügen Sie nun ein Mock-Objekt für die semantische Suche hinzu:

function createSemanticSearchMock(query, indexName) {
  mock.add(
    {
      method: "POST",
      path: `/${indexName}/_search`,
      body: {
        query: {
          semantic: {
            field: "semantic_field",
            query: query,
          },
        },
      },
    },
    () => {
      return {
        hits: {
          total: { value: 2, relation: "eq" },
          hits: [
            {
              _id: "1",
              _score: 0.9,
              _source: {
                owner_name: "Alice Johnson",
                pet_name: "Buddy",
                species: "Dog",
                breed: "Golden Retriever",
                vaccination_history: ["Rabies", "Parvovirus", "Distemper"],
                visit_details:
                  "Annual check-up and nail trimming. Healthy and active.",
              },
            },
            {
              _id: "2",
              _score: 0.7,
              _source: {
                owner_name: "Daniel Kim",
                pet_name: "Mochi",
                species: "Rabbit",
                breed: "Mixed",
                vaccination_history: [],
                visit_details:
                  "Nail trimming and general health check. No issues.",
              },
            },
          ],
        },
      };
    }
  );
}

Wir können nun einen Test für unseren Code erstellen, um sicherzustellen, dass der Elasticsearch-Teil immer die gleichen Ergebnisse liefert:

import test from 'ava';

test("performSemanticSearch must return formatted results correctly", async (t) => {
  const indexName = "vet-visits";
  const query = "Which pets had nail trimming?";

  createSemanticSearchMock(query, indexName);

  async function performSemanticSearch(esClient, q, indexName = "vet-visits") {
    try {
      const result = await esClient.search({
        index: indexName,
        body: {
          query: {
            semantic: {
              field: "semantic_field",
              query: q,
            },
          },
        },
      });

      return {
        success: true,
        results: result.hits.hits,
      };
    } catch (error) {
      if (error instanceof errors.TimeoutError) {
        return {
          success: false,
          results: null,
          error: error.body.error.reason,
        };
      }

      return {
        success: false,
        results: null,
        error: error.message,
      };
    }
  }

  const result = await performSemanticSearch(esClient, query, indexName);

  t.true(result.success, "The search must be successful");
  t.true(Array.isArray(result.results), "The results must be an array");

  if (result.results.length > 0) {
    t.true(
      "_source" in result.results[0],
      "Each result must have a _source property"
    );
    t.true(
      "pet_name" in result.results[0]._source,
      "Results must include the pet_name field"
    );
    t.true(
      "visit_details" in result.results[0]._source,
      "Results must include the visit_details field"
    );
  }
});

Lasst uns die Tests durchführen.

npm run test

Erledigt! Ab sofort können wir unsere App testen und uns dabei zu 100 % auf den Code konzentrieren, ohne uns von externen Faktoren beeinflussen zu lassen.

Serverlose Umgebungen

Wie Sie den Client auf Elastic Serverless ausführen

Wir haben die Ausführung von Elasticsearch in der Cloud oder lokal behandelt; der Node.js-Client unterstützt jedoch auch Verbindungen zu Elastic Cloud Serverless.

Mit Elastic Cloud Serverless können Sie ein Projekt erstellen, bei dem Sie sich keine Gedanken um die Infrastruktur machen müssen, da Elastic dies intern übernimmt. Sie müssen sich nur um die Daten kümmern, die Sie indizieren möchten, und darum, wie lange Sie darauf zugreifen möchten.

Aus Nutzersicht entkoppelt Serverless Rechenleistung von Speicher und bietet automatische Skalierungsfunktionen sowohl für die Suche als auch für die Indizierung. Dadurch können Sie nur die Ressourcen anbauen, die Sie tatsächlich benötigen.

Der Client nimmt folgende Anpassungen vor, um eine Verbindung zu Serverless herzustellen:

  • Deaktiviert das Sniffing und ignoriert alle Sniffing-bezogenen Optionen.

  • Ignoriert alle in der Konfiguration übergebenen Knoten außer dem ersten und ignoriert jegliche Knotenfilter- und Auswahloptionen.

  • Aktiviert Komprimierung und `TLSv1_2_method` (genau wie bei der Konfiguration für Elastic Cloud).

  • Fügt allen Anfragen einen HTTP-Header `elastic-api-version` hinzu

  • Verwendet standardmäßig `CloudConnectionPool` anstelle von `WeightedConnectionPool`.

  • Deaktiviert die vom Anbieter bereitgestellten `content-type`- und `accept`-Header zugunsten der Standard-MIME-Typen.

Um Ihr serverloses Projekt zu verbinden, müssen Sie den Parameter serverMode: serverless verwenden.

const { Client } = require('@elastic/elasticsearch')
const client = new Client({
  node: 'ELASTICSEARCH_ENDPOINT',
  auth: { apiKey: 'ELASTICSEARCH_API_KEY' },
  serverMode: "serverless",
});

Wie Sie den Client in einer Function-as-a-Service-Umgebung ausführen

In diesem Beispiel haben wir einen Node.js-Server verwendet, aber Sie können auch über eine Function-as-a-Service-Umgebung mit Funktionen wie AWS Lambda, GCP Run usw. eine Verbindung herstellen.

'use strict'

const { Client } = require('@elastic/elasticsearch')

const client = new Client({
  // client initialisation
})

exports.handler = async function (event, context) {
  // use the client
}

Ein weiteres Beispiel ist die Anbindung an Dienste wie Vercel, das ebenfalls serverlos ist. Sie können sich dieses vollständige Beispiel ansehen, wie das geht, aber der relevanteste Teil des Suchendpunkts sieht folgendermaßen aus:

const response = await client.search(
  {
    index: INDEX,
    // You could directly send from the browser
    // the Elasticsearch's query DSL, but it will
    // expose you to the risk that a malicious user
    // could overload your cluster by crafting
    // expensive queries.
    query: {
      match: { field: req.body.text },
    },
  },
  {
    headers: {
      Authorization: `ApiKey ${token}`,
    },
  }
);

Dieser Endpunkt befindet sich im Ordner /api und wird serverseitig ausgeführt, sodass der Client nur über den Parameter „text“ verfügt, der dem Suchbegriff entspricht.

Die Konsequenz aus der Verwendung von Function-as-a-Service ist, dass Funktionen, im Gegensatz zu einem Server, der rund um die Uhr läuft, nur die Maschine starten, auf der die Funktion ausgeführt wird, und dass die Maschine nach Beendigung der Funktion in den Ruhemodus wechselt, um weniger Ressourcen zu verbrauchen.

Diese Konfiguration kann praktisch sein, wenn die Anwendung nicht zu viele Anfragen erhält; andernfalls können die Kosten hoch sein. Sie müssen auch den Lebenszyklus der Funktionen und die Laufzeiten berücksichtigen (die in manchen Fällen nur Sekunden betragen können).

Fazit

In diesem Artikel haben wir gelernt, wie man mit Fehlern umgeht, was in Produktionsumgebungen von entscheidender Bedeutung ist. Wir haben auch das Testen unserer Anwendung unter Verwendung von Mocking für den Elasticsearch-Dienst behandelt. Dies ermöglicht zuverlässige Tests unabhängig vom Zustand des Clusters und erlaubt es uns, uns auf unseren Code zu konzentrieren.

Zum Schluss demonstrierten wir, wie man einen vollständig serverlosen Stack einrichtet, indem man sowohl Elastic Cloud Serverless als auch eine Vercel-Anwendung bereitstellt.

Zugehörige Inhalte

Entwicklung eines agentenbasierten RAG-Assistenten mit JavaScript und Mastra und Elasticsearch

JD Armada

Elasticsearch in JavaScript richtig anwenden, Teil 1

Jeffrey Rengifo

Sind Sie bereit, hochmoderne Sucherlebnisse zu schaffen?

Eine ausreichend fortgeschrittene Suche kann nicht durch die Bemühungen einer einzelnen Person erreicht werden. Elasticsearch wird von Datenwissenschaftlern, ML-Ops-Experten, Ingenieuren und vielen anderen unterstützt, die genauso leidenschaftlich an der Suche interessiert sind wie Sie. Lasst uns in Kontakt treten und zusammenarbeiten, um das magische Sucherlebnis zu schaffen, das Ihnen die gewünschten Ergebnisse liefert.

Probieren Sie es selbst aus