Einleitung
Grundsätzlich ist OpenSearch ein Framework für Suchaufgaben und Datenanalyse. Dabei unterstützt es primär das Auffinden von Daten in sehr großen Datapools und kann bei Analyseaufgaben unterstützen. Es ist primär keine Dokumentendatenbank zum Persistieren von Datenstrukturen. Allerdings ist OpenSearch für diese hybride Anforderung vorbereitet und unterstützt das mit vielen Werkzeugen. Insbesondere wenn die Datenstrukturen von Natur aus als JSON (und vor allem strukturiert) vorliegen, bietet es sich an, ausschließlich OpenSearch zur Speicherung zu nutzen, ohne weitere Storages zu verwenden. Das Bild ändert sich, wenn die Daten nicht natürlicherweise in JSON vorliegen, vornehmlich binäre Dokumente (OpenOffice, MS Office, PDF, usw. usf.). OpenSearch bietet da keine sinnvolle Möglichkeit an. Es ist zwar möglich ein Dokument Base64-Encoded in das JSON einzubetten, aber damit tun wir uns keinen Gefallen. Auch wenn es schon Persistenzen der Quelldaten gibt (wie z.B. RDBMS) gibt, dupliziert man nicht alles nach OpenSearch, wenn es der Suche und Analyse nicht dient. In dem Fall hinterlegt man im JSON Datensatz eine Referenz (URI, Object-ID, UUID, …) und verweist somit auf die Originaldaten. Hat man so eine Persistenz nicht, kann es auch ein Pfad zu einer Datei sein oder besser ein BlobStorage.
Ich möchte aber hier doch auf den Ansatz eingehen, der strukturierte JSON Daten komplett in die Hände von OpenSearch gibt. Ich werde auch mal ein Artikel zur Datensicherung und Wiederherstellung schreiben, damit man sich nicht den Kritikpunkt aussetzen muss, man arbeite ansonsten mit sehr flüchtigen Daten.
Mastodon Toots speichern
Im Nachbar-Blog BeanDev: Mastodon beschäftige ich mich mit der Mastodon API und der Bibliothek Mastodon.py. Es wäre zu empfehlen, da mal reinzuschauen, weil einige Dinge von dort übernommen werden. In den folgenden Beispielen importiere ich einfach einige Toots der Local Timeline. Der Vorteil ist, dass es dafür i.d.R. keinerlei Authentifizierung benötigt.
Das Auslesen ist sehr trivial:
from mastodon import Mastodon
from opensearchpy import OpenSearch
mastodon = Mastodon (
api_base_url='https://social.tchncs.de'
)
local = mastodon.timeline_local()
In local
befinden sich damit die letzten 20 Toots. Die Mastodon API erzwingt das Paging (ausgenommen bei den Streams) und die Größe einer Page kann nur bis 40 Objekte ausgedehnt werden.
Die API unterstützt mit ein paar Hilfsfunktionen das Auslesen weiterer Seiten. Das machen wir auch ein paar mal, damit es ein paar Dokumente gibt:
...
page = None
for _ in range(5):
page = mastodon.timeline_local(limit=40) if page is None else mastodon.fetch_next(page)
Das ist einfach genug für unser Beispiel. Nun müssen wir das in unseren toot
-Index bekommen, den wir im letzten Blog-Artikel angelegt hatten.
Die OpenSearch.py API unterstützt auch dabei und da wir das JSON Dokument eins zu eins übertragen, ist es schon fast langweilig:
from mastodon import Mastodon
from opensearchpy import OpenSearch
mastodon = Mastodon (
api_base_url='https://social.tchncs.de'
)
host = 'localhost'
port = 9201
client = OpenSearch(
hosts = [{'host': host, 'port': port}],
http_compress = True, # enables gzip compression for request bodies
use_ssl = False
)
# Der Name des Index
index_name = 'toots'
page = None
for _ in range(5):
page = mastodon.timeline_local(limit=40) if page is None else mastodon.fetch_next(page)
for toot in page:
client.index(index_name, body=toot)
Das war es schon. Es sollten nun 200 Toots in unserem Index sein, fragen wir mal nach http://localhost:9200/_cat/indices?v:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open toots -UoFyWfnStybxDe4-gB3-Q 4 0 0 0 93.3kb 93.3kb
green open .kibana_1 oPhh4WkXTha54EXU4SdJ7A 1 0 1 0 5kb 5kb
Hm, docs.count = 0
?
Ok, da funktioniert noch etwas nicht, aber ich löse mal auf: Der Befehl ist grundsätzlich erstmal eine nette Aufforderung. Die index
Methode erlaubt noch einen Parameter `refresh=True’ zu setzen, das sollte man aber im Batch nicht machen. Das kostet zu viel Performance, wenn wir unmittelbar für die einzelnen Dokumente nicht an dem Ergebnis interessiert sind.
Man sollte also nach dem Lauf durch alle 200 Toots einen expliziten Refresh durchführen.
client.indices.refresh(index=index_name)
Nun endlich sehen wir etwas http://localhost:9200/_cat/indices?v:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open toots 14ADrQSgSgeXEWDxzO2o3A 4 0 200 0 840.3kb 840.3kb
green open .kibana_1 oPhh4WkXTha54EXU4SdJ7A 1 0 1 0 5kb 5kb
200 Toots verbrauchen 840,8 kB an Daten.
Dann schauen wir erst mal nach, was mit unserem Mapping passiert ist. Wie schon erwähnt, übernimmt OpenSearch eine Menge an Mapping Arbeit, wenn wir das nicht explizit festgelegt haben:
Die Information über den Index bekommen wir mit http://localhost:9200/toots/ und die Ausgabe hat sich nun gewaltig verändert. Zig Attribute wurden von OpenSearch entdeckt. Bei einigen kann man mitgehen, was das Mapping betrifft, bei anderen Attributen klappt das überhaupt nicht.
Gerade die Attribute, die oft leer sind und dann leere Arrays oder als null
im Status-Object sind, werden von OpenSearch als text
interpretiert. Die selbst gesetzten Mappings wurden natürlich nicht überschrieben.
Aber egal, darum kümmern wir uns später. Schauen wir mal nach den Dokumenten http://localhost:9200/toots/_search?pretty=true:
{
"took" : 41,
"timed_out" : false,
"_shards" : {
"total" : 4,
"successful" : 4,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 200,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
...
Der Aufruf brauchte 41ms, die Anfrage ging über alle 4 shards (das sind unsere Apache Lucene Prozesse) und alle haben reagiert. Die totale Anzahl wären 200 Dokumente. Mit dem Array hits[] folgen die Ergebnisse. Dort sind aber nur 10 aufgelistet. Auch OpenSearch hat Limits. Hängt man den &size=n
Parameter an, kann man mehr Ergebnisse bekommen. Allerdings auch nicht mehr, als in der globalen Konfiguration hinterlegt ist. Wenn man das wirklich braucht, gibt es spezielle Streaming-Möglichkeiten. Aber das ist eher unwahrscheinlich.
Schaut man sich den ersten Toot an, sieht man erst ein paar Meta-Informationen mit einem _:
"hits" : [
{
"_index" : "toots",
"_type" : "_doc",
"_id" : "tSibmYABzTa-ID4sDwiJ",
"_score" : 1.0,
"_source" : {
"id" : 108255279217081900,
"created_at" : "2022-05-06T13:41:10.477000+00:00",
"in_reply_to_id" : 108254896893762953,
"in_reply_to_account_id" : 107769893804592435,
"sensitive" : false,
...
Es wird der Index angegeben (man kann nämlich auf mehrere Indexe gleichzeitig abfragen und dann braucht man die Unterscheidung), den Typ _doc
. Dann eine _id
, die eine Zeichenfolge ist und nichts mit der Status-ID von Mastodon zu tun hat. Darüber müssen wir später noch reden. Der Score sagt uns, dass zu unserer Anfrage ein Volltreffer gelandet wurde (kein Wunder, wir haben ja keine Einschränkungen gesetzt) und dann folgt endlich mit _source
unser Toot. Schaut man da tiefer rein, ist das ziemlich vergleichbar mit dem Original, auch die HTML Tags sind weiterhin da (siehe content
).
Jetzt hängt es etwas von den Testdaten, die in eurem OpenSearch gelandet sind, weil die Local Timeline sich verändert. Man kann aber mal ein paar Suchanfragen ausprobieren:
- http://localhost:9200/toots/_search?pretty=true&size=20&q=mastodon
- Das liefert wohl viele Ergebnisse, weil
Mastodon
fast überall drin steht.
- Das liefert wohl viele Ergebnisse, weil
Ok, wie ich es schon angekündigt hatte. Meistens findet man mehr als man eigentlich wollte. Besonders die Suche über alle Felder ist nicht hilfreich.
- http://localhost:9200/toots/_search?pretty=true&size=20&q=content=fediverse
- Schon besser. Bei mir sind es nur noch 6 Treffer
Das mit der Suche über die URL mit dem q=
Parameter bietet leider nicht alle Möglichkeiten, die OpenSearch in seinem Repertoire hat. Deswegen ist es sinnvoller, mit der API zu arbeiten, die JSON DSL Abfrage-Objekte im Request Body verwendet. Das geht über den Browser sowieso nicht und könnte nur mit Tools wie Postman realisiert werden. Zu der Query API kommen wir in einem anderen Artikel.
Ein weiteres Problem gibt es, wenn wir erneut Toots aus einer Timeline importieren. Eventuell erwischen wir einen großen Teil der alten Status-Nachrichten, und die würde OpenSearch erneut anlegen. Der Grund ist, dass wir die _id
von OpenSearch generieren lassen. Es gibt nur einen empfohlenen Weg, man setzt selbst die ID:
page = None
for _ in range(5):
page = mastodon.timeline_local(limit=40) if page is None else mastodon.fetch_next(page)
for toot in page:
id = toot['id']
client.index(index_name, id=id, body=toot)
Nun sieht man, dass alle _id
mit der _source.id
korrespondieren.
Würde man das Script mehrere Male hintereinander aufrufen, erhöht sich die Zahl der Dokumente dann nicht jedes Mal um 200, sondern nur um 1 oder 2 oder vielleicht gar nicht. Je nachdem, was auf der Timeline los ist. D.h. es werden keine Duplikate mehr angelegt.
’ Dokumente oder Index löschen
Bei dem vielen Testen will man auch mal aufräumen. Das geht einfach:
from opensearchpy import OpenSearch
host = 'localhost'
port = 9201
# Client zu dem Dev Cluster (ohne SSL, ohne Anmeldung)
client = OpenSearch(
hosts = [{'host': host, 'port': port}],
http_compress = True, # enables gzip compression for request bodies
use_ssl = False
)
# Der Name des Index
index_name = 'toots'
all={
"query": { "match_all": {} }
}
client.delete_by_query(index=index_name, body=all, refresh=True)
Das sieht man dann unmittelbar mit http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open toots Umfh_0jMTCabcNOXWbB29Q 4 0 0 201 1.9mb 1.9mb
green open .kibana_1 oPhh4WkXTha54EXU4SdJ7A 1 0 1 0 5kb 5kb
Es ist sinnvoll sich ein paar Scripte mit dem Erstellen eines Index, Löschen von Dokumenten oder auch Löschen eines kompletten Index (was auch das Mapping entfernt) durchführt.
Löschen eines Index ist noch radikaler:
...
# Der Name des Index
index_name = 'toots'
client.indices.delete(index=index_name)
Danach muss man aber den Index neu anlegen, bevor man wieder Daten hinzufügt.
Im nächsten Artikel steige ich in die Query-API ein. Das Mapping muss auch nochmal komplett überarbeitet werden, damit wir sinnvoller Suchen und Finden können.
Comments
No comments yet. Be the first to react!