Heim > Backend-Entwicklung > Golang > Bleve: Wie baue ich eine raketenschnelle Suchmaschine?

Bleve: Wie baue ich eine raketenschnelle Suchmaschine?

Susan Sarandon
Freigeben: 2025-01-03 04:23:40
Original
1007 Leute haben es durchsucht

Bleve: How to build a rocket-fast search engine?

Go/Golang ist eine meiner Lieblingssprachen; Ich liebe den Minimalismus und wie sauber er ist, er ist syntaktisch sehr kompakt und bemüht sich sehr, die Dinge einfach zu halten (ich bin ein großer Fan des KISS-Prinzips).

Eine der größten Herausforderungen, vor denen ich in letzter Zeit stand, war der Aufbau einer schnellen Suchmaschine. Natürlich gibt es Optionen wie SOLR und ElasticSearch; Beide funktionieren wirklich gut und sind hoch skalierbar. Allerdings musste ich die Suche vereinfachen, indem ich sie schneller und einfacher bereitzustellen und mit wenig bis gar keinen Abhängigkeiten ausstatte.

Ich musste so weit optimieren, dass ich schnell Ergebnisse liefern konnte, damit sie neu eingestuft werden konnten. Obwohl C/Rust hierfür gut geeignet sein könnte, schätze ich Entwicklungsgeschwindigkeit und Produktivität. Golang ist meiner Meinung nach das Beste aus beiden Welten.

In diesem Artikel werde ich ein einfaches Beispiel dafür durchgehen, wie Sie mit Go Ihre eigene Suchmaschine erstellen können. Sie werden überrascht sein: Es ist gar nicht so kompliziert, wie Sie vielleicht denken.

Golang: Python über Steroide

Ich weiß nicht warum, aber Golang fühlt sich in gewisser Weise wie Python an. Die Syntax ist sehr leicht zu verstehen, vielleicht liegt es am Fehlen von Semikolons und Klammern überall oder am Fehlen hässlicher Try-Catch-Anweisungen. Vielleicht ist es der tolle Go-Formatierer, ich weiß es nicht.

Wie auch immer, da Golang eine einzige eigenständige Binärdatei generiert, ist die Bereitstellung auf jedem Produktionsserver super einfach. Sie gehen einfach zum „Build“ und tauschen die ausführbare Datei aus.

Das ist genau das, was ich brauchte.

Bleve?

Nein, das ist kein Tippfehler? Bleve ist eine leistungsstarke, benutzerfreundliche und sehr flexible Suchbibliothek für Golang.

Während Sie als Go-Entwickler im Allgemeinen Drittanbieterpakete wie die Pest meiden; Manchmal ist es sinnvoll, ein Paket eines Drittanbieters zu verwenden. Bleve ist schnell, gut gestaltet und bietet ausreichend Wert, um seine Verwendung zu rechtfertigen.

Außerdem ist hier der Grund, warum ich „Bleve“ habe:

  • Eigenständig, einer der großen Vorteile von Golang ist die einzelne Binärdatei, daher wollte ich dieses Gefühl beibehalten und keine externe Datenbank oder einen externen Dienst zum Speichern und Abfragen von Dokumenten benötigen. Bleve läuft im Speicher und schreibt auf die Festplatte, ähnlich wie SQLite.

  • Einfach zu erweitern. Da es sich nur um Go-Code handelt, kann ich die Bibliothek bei Bedarf problemlos optimieren oder in meiner Codebasis erweitern.

  • Schnell: Suchergebnisse in 10 Millionen Dokumenten dauern nur 50–100 ms, einschließlich Filterung.

  • Facettierung: Sie können keine moderne Suchmaschine ohne ein gewisses Maß an Facettierungsunterstützung erstellen. Bleve bietet volle Unterstützung für gängige Facettentypen: wie Bereiche oder einfache Kategorieanzahlen.

  • Schnelle Indizierung: Bleve ist etwas langsamer als SOLR. SOLR kann 10 Millionen Dokumente in 30 Minuten indizieren, während Bleve über eine Stunde benötigt, aber eine Stunde oder so ist für meine Bedürfnisse immer noch ziemlich anständig und schnell genug.

  • Gute Qualitätsergebnisse. Bleve schneidet mit den Keyword-Ergebnissen gut ab, aber auch einige semantische Suchvorgänge funktionieren in Bleve sehr gut.

  • Schneller Start: Wenn Sie neu starten oder ein Update bereitstellen müssen, dauert der Neustart von Bleve nur Millisekunden. Es gibt keine Blockierung von Lesevorgängen, um den Index im Speicher neu aufzubauen, sodass die Suche im Index nur Millisekunden nach einem Neustart ohne Probleme möglich ist.

Einen Index einrichten?

In Bleve kann man sich einen „Index“ als eine Datenbanktabelle oder eine Sammlung (NoSQL) vorstellen. Im Gegensatz zu einer regulären SQL-Tabelle müssen Sie nicht jede einzelne Spalte angeben, sondern können für die meisten Anwendungsfälle grundsätzlich mit dem Standardschema auskommen.

Um einen Bleve-Index zu initialisieren, können Sie Folgendes tun:

mappings := bleve.NewIndexMapping()
index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil)
if err != nil {
    log.Fatal(err)
}
Nach dem Login kopieren
Nach dem Login kopieren

Bleve unterstützt einige verschiedene Indextypen, aber nach langem Hin und Her habe ich herausgefunden, dass der Indextyp „scorch“ die beste Leistung bietet. Wenn Sie die letzten drei Argumente nicht übergeben, verwendet Bleve standardmäßig BoltDB.

Dokumente hinzufügen

Das Hinzufügen von Dokumenten zu Bleve ist ein Kinderspiel. Grundsätzlich können Sie jede Art von Struktur im Index speichern:

type Book struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Genre string `json:"genre"`
}

b := Book{
    ID:    1234,
    Name:  "Some creative title",
    Genre: "Young Adult",
}
idStr := fmt.Sprintf("%d", b.ID)
// index(string, interface{})
index.index(idStr, b)
Nach dem Login kopieren
Nach dem Login kopieren

Wenn Sie eine große Menge an Dokumenten indizieren, ist es besser, die Stapelverarbeitung zu verwenden:

// You would also want to check if the batch exists already
// - so that you don't recreate it.
batch := index.NewBatch()
if batch.Size() >= 1000 {
    err := index.Batch(batch)
    if err != nil {
        // failed, try again or log etc...
    }
    batch = index.NewBatch()
} else {
    batch.index(idStr, b)
}
Nach dem Login kopieren
Nach dem Login kopieren

Wie Sie feststellen werden, wird eine komplexe Aufgabe wie das Stapeln von Datensätzen und deren Schreiben in den Index durch die Verwendung von „index.NewBatch“ vereinfacht, das einen Container zum temporären Indizieren von Dokumenten erstellt.

Danach überprüfen Sie einfach die Größe, während Sie eine Schleife durchlaufen, und leeren den Index, sobald Sie die Stapelgrößenbeschränkung erreicht haben.

Durchsuchen des Index

Bleve stellt mehrere verschiedene Parser für Suchanfragen zur Verfügung, aus denen Sie je nach Ihren Suchanforderungen auswählen können. Um diesen Artikel kurz und bündig zu halten, verwende ich einfach den Standard-Query-String-Parser.

searchParser := bleve.NewQueryStringQuery("chicken reciepe books")
maxPerPage := 50
ofsset := 0
searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false)
// By default bleve returns just the ID, here we specify
// - all the other fields we would like to return.
searchRequest.Fields = []string{"id", "name", "genre"}
searchResults, err := index.Search(searchResult)
Nach dem Login kopieren
Nach dem Login kopieren

Mit nur diesen wenigen Zeilen verfügen Sie jetzt über eine leistungsstarke Suchmaschine, die bei geringem Speicher- und Ressourcenbedarf gute Ergebnisse liefert.

Hier ist eine JSON-Darstellung der Suchergebnisse. „Hits“ enthält die passenden Dokumente:

{
    "status": {
        "total": 5,
        "failed": 0,
        "successful": 5
    },
    "request": {},
    "hits": [],
    "total_hits": 19749,
    "max_score": 2.221337297308545,
    "took": 99039137,
    "facets": null
}
Nach dem Login kopieren
Nach dem Login kopieren

Facettieren

Wie bereits erwähnt, bietet Bleve sofort vollständige Facettierungsunterstützung, ohne dass diese in Ihrem Schema eingerichtet werden muss. Um beispielsweise das Buch „Genre“ zu facettieren, können Sie Folgendes tun:

//... build searchRequest -- see previous section.
// Add facets
genreFacet := bleve.NewFacetRequest("genre", 50)
searchRequest.AddFacet("genre", genreFacet)
searchResults, err := index.Search(searchResult)
Nach dem Login kopieren
Nach dem Login kopieren

Wir erweitern unseren searchRequest von früher mit nur 2 Codezeilen. Die „NewFacetRequest“ akzeptiert zwei Argumente:

  • Feld: das Feld in unserem Index, auf das (String) facettiert werden soll.

  • Größe: die Anzahl der zu zählenden Einträge (Ganzzahl). In unserem Beispiel werden also nur die ersten 50 Genres gezählt.

Das Obige füllt nun die „Facetten“ in unseren Suchergebnissen aus.

Als nächstes fügen wir einfach unsere Facette zur Suchanfrage hinzu. Dazu gehören ein „Facettenname“ und die tatsächliche Facette. „Facettenname“ ist der „Schlüssel“, unter dem Sie diese Ergebnismenge in unseren Suchergebnissen finden.

Erweiterte Abfragen und Filterung

Mit dem Parser „QueryStringQuery“ können Sie zwar einiges erreichen; Manchmal benötigen Sie komplexere Abfragen wie „eine muss übereinstimmen“, bei der Sie einen Suchbegriff mit mehreren Feldern abgleichen und Ergebnisse zurückgeben möchten, solange mindestens ein Feld übereinstimmt.

Sie können dazu die Abfragetypen „Disjunktion“ und „Konjunktion“ verwenden.

  • Konjunktionsabfrage: Im Grunde ermöglicht es Ihnen, mehrere Abfragen zu einer einzigen riesigen Abfrage zu verketten. Alle untergeordneten Abfragen müssen mit mindestens einem Dokument übereinstimmen.

  • Disjunktionsabfrage: Damit können Sie die oben erwähnte „Eins muss übereinstimmen“-Abfrage durchführen. Sie können eine x-Anzahl an Abfragen übergeben und festlegen, wie viele untergeordnete Abfragen mit mindestens einem Dokument übereinstimmen müssen.

Beispiel für eine Disjunktionsabfrage:

mappings := bleve.NewIndexMapping()
index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil)
if err != nil {
    log.Fatal(err)
}
Nach dem Login kopieren
Nach dem Login kopieren

Ähnlich wie wir zuvor „searchParser“ verwendet haben, können wir jetzt die „Disjunction Query“ an den Konstruktor für unsere „searchRequest“ übergeben.

Obwohl es nicht genau dasselbe ist, ähnelt es dem folgenden SQL:

type Book struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Genre string `json:"genre"`
}

b := Book{
    ID:    1234,
    Name:  "Some creative title",
    Genre: "Young Adult",
}
idStr := fmt.Sprintf("%d", b.ID)
// index(string, interface{})
index.index(idStr, b)
Nach dem Login kopieren
Nach dem Login kopieren

Sie können auch anpassen, wie unscharf die Suche sein soll, indem Sie „query.Fuzziness=[0 oder 1 oder 2]“ festlegen

Beispiel für eine Konjunktionsabfrage:

// You would also want to check if the batch exists already
// - so that you don't recreate it.
batch := index.NewBatch()
if batch.Size() >= 1000 {
    err := index.Batch(batch)
    if err != nil {
        // failed, try again or log etc...
    }
    batch = index.NewBatch()
} else {
    batch.index(idStr, b)
}
Nach dem Login kopieren
Nach dem Login kopieren

Sie werden feststellen, dass die Syntax sehr ähnlich ist. Sie können die Abfragen „Konjunktion“ und „Disjunktion“ grundsätzlich austauschbar verwenden.

Dies wird in SQL etwa wie folgt aussehen:

searchParser := bleve.NewQueryStringQuery("chicken reciepe books")
maxPerPage := 50
ofsset := 0
searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false)
// By default bleve returns just the ID, here we specify
// - all the other fields we would like to return.
searchRequest.Fields = []string{"id", "name", "genre"}
searchResults, err := index.Search(searchResult)
Nach dem Login kopieren
Nach dem Login kopieren

Zusammenfassend; Verwenden Sie die „Konjunktionsabfrage“, wenn Sie möchten, dass alle untergeordneten Abfragen mit mindestens einem Dokument übereinstimmen, und die „Disjunktionsabfrage“, wenn Sie mit mindestens einer untergeordneten Abfrage, aber nicht unbedingt mit allen untergeordneten Abfragen übereinstimmen möchten.

Sharding

Wenn Sie auf Geschwindigkeitsprobleme stoßen, ermöglicht Bleve auch die Verteilung Ihrer Daten auf mehrere Index-Shards und die anschließende Abfrage dieser Shards in einer Anfrage, zum Beispiel:

{
    "status": {
        "total": 5,
        "failed": 0,
        "successful": 5
    },
    "request": {},
    "hits": [],
    "total_hits": 19749,
    "max_score": 2.221337297308545,
    "took": 99039137,
    "facets": null
}
Nach dem Login kopieren
Nach dem Login kopieren

Sharding kann ziemlich komplex werden, aber wie Sie oben sehen, nimmt Bleve einen Großteil des Aufwands ab, da es automatisch alle Indizes und Suchvorgänge „zusammenführt“ und dann Ergebnisse in einem Ergebnissatz zurückgibt, genau so, als ob Sie gesucht hätten ein einzelner Index.

Ich habe Sharding verwendet, um 100 Shards zu durchsuchen. Der gesamte Suchvorgang dauert durchschnittlich nur 100–200 Millisekunden.

Sie können Shards wie folgt erstellen:

//... build searchRequest -- see previous section.
// Add facets
genreFacet := bleve.NewFacetRequest("genre", 50)
searchRequest.AddFacet("genre", genreFacet)
searchResults, err := index.Search(searchResult)
Nach dem Login kopieren
Nach dem Login kopieren

Stellen Sie einfach sicher, dass Sie für jedes Dokument eindeutige IDs erstellen oder über eine vorhersehbare Möglichkeit verfügen, Dokumente hinzuzufügen und zu aktualisieren, ohne den Index durcheinander zu bringen.

Eine einfache Möglichkeit, dies zu tun, besteht darin, ein Präfix zu speichern, das den Shard-Namen in Ihrer Quelldatenbank enthält, oder wo auch immer Sie die Dokumente beziehen. So dass Sie jedes Mal, wenn Sie versuchen, etwas einzufügen oder zu aktualisieren, das „Präfix“ nachschlagen, das Ihnen sagt, auf welchem ​​Shard Sie „.index“ aufrufen müssen.

Apropos Aktualisieren: Durch den einfachen Aufruf von „index.index(idstr, struct)“ wird ein vorhandenes Dokument aktualisiert.

Abschluss

Wenn Sie nur diese grundlegende Suchtechnik oben verwenden und sie hinter GIN oder dem standardmäßigen Go-HTTP-Server einfügen, können Sie eine ziemlich leistungsstarke Such-API erstellen und Millionen von Anfragen bedienen, ohne eine komplexe Infrastruktur einführen zu müssen.

Eine Einschränkung allerdings; Bleve bietet jedoch keine Replikation, da Sie diese in eine API einbinden können. Führen Sie einfach einen Cron-Job aus, der aus Ihrer Quelle liest und mithilfe von Goroutinen ein Update auf alle Ihre Bleve-Server „ausspielt“.

Alternativ können Sie das Schreiben auf die Festplatte einfach für ein paar Sekunden sperren und dann die Daten einfach per „rsync“ auf die Slave-Indizes übertragen. Ich rate jedoch davon ab, da Sie wahrscheinlich jedes Mal auch die Go-Binärdatei neu starten müssten .

Das obige ist der detaillierte Inhalt vonBleve: Wie baue ich eine raketenschnelle Suchmaschine?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage