Einen großartigen Nest.js-Blog erstellen: Volltextsuche für Beiträge
Emily Parker
Product Engineer · Leapcell

Im vorherigen Tutorial haben wir unseren Blogbeiträgen eine Funktion zum Hochladen von Bildern hinzugefügt.
Mit der Zeit stellen Sie sich wahrscheinlich vor, dass Ihr Blog eine ganze Reihe von Artikeln angesammelt hat. Ein neues Problem tritt allmählich auf: Wie können Leser schnell die Artikel finden, die sie lesen möchten?
Die Antwort ist natürlich die Suche.
In diesem Tutorial werden wir unserem Blog eine Volltextsuchfunktion hinzufügen.
Sie denken vielleicht: Kann ich nicht einfach eine SQL-Abfrage LIKE '%keyword%'
verwenden, um die Suche zu implementieren? Für einfache Szenarien können Sie das sicherlich. LIKE
-Abfragen sind jedoch bei der Verarbeitung großer Textblöcke leistungsschwach und verstehen keine sprachlichen Komplexitäten (z. B. wird bei der Suche nach "create" nicht "creating" gefunden).
Daher werden wir eine professionellere und effizientere Lösung wählen: die Nutzung der eingebauten Volltextsuchfunktion (FTS) von PostgreSQL. Sie ist nicht nur blitzschnell, sondern behandelt auch Stemming, ignoriert Stoppwörter und sortiert nach Relevanz, was ein weit überlegenes Sucherlebnis im Vergleich zu LIKE
bietet.
Schritt 1: Datenbank-Suchinfrastruktur
Um die FTS-Funktion von PostgreSQL nutzen zu können, müssen wir zunächst einige Modifikationen an unserer post
-Tabelle vornehmen. Die Kernidee ist, eine spezielle Spalte zu erstellen, die optimierte Textdaten speichert, die mit hoher Geschwindigkeit durchsucht werden können.
1. Kernkonzept: tsvector
Wir werden der post
-Tabelle eine neue Spalte vom Typ tsvector
hinzufügen. Sie können sich das als ein "Suchwörterbuch" vorstellen. Es zerlegt den Titel und den Inhalt des Artikels in einzelne Wörter (Lexeme) und standardisiert sie (z. B. verarbeitet es sowohl "running" als auch "ran" zu "run") für nachfolgende Abfragen.
2. Ändern der Tabellenstruktur
Stellen Sie eine Verbindung zu Ihrer PostgreSQL-Datenbank her und führen Sie die folgende SQL-Anweisung aus, um die Spalte search_vector
zur post
-Tabelle hinzuzufügen.
ALTER TABLE "post" ADD COLUMN "search_vector" tsvector;
3. Automatische Aktualisierungen mit einem Trigger
Natürlich möchten wir diese search_vector
-Spalte nicht jedes Mal manuell aktualisieren, wenn wir einen Beitrag erstellen oder aktualisieren. Der beste Weg ist, die Datenbank diese Arbeit automatisch erledigen zu lassen. Wir erreichen dies durch die Erstellung eines Triggers.
Erstellen Sie zunächst eine Funktion, deren Zweck es ist, den title
und den content
zu verketten und sie in das tsvector
-Format zu konvertieren.
CREATE OR REPLACE FUNCTION update_post_search_vector() RETURNS TRIGGER AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') || setweight(to_tsvector('english', coalesce(NEW.content, '')), 'B'); RETURN NEW; END; $$ LANGUAGE plpgsql;
Tipp: Die Funktion
setweight
ermöglicht es Ihnen, Text aus verschiedenen Feldern unterschiedliche Gewichte zuzuweisen. Hier haben wir dem Titel ('A') ein höheres Gewicht als dem Inhalt ('B') gegeben, was bedeutet, dass Artikel mit dem Schlüsselwort im Titel in den Suchergebnissen höher eingestuft werden.
Erstellen Sie als Nächstes einen Trigger, der die gerade erstellte Funktion jedes Mal automatisch aufruft, wenn ein neuer Beitrag eingefügt (INSERT
) oder aktualisiert (UPDATE
) wird.
CREATE TRIGGER post_search_vector_update BEFORE INSERT OR UPDATE ON "post" FOR EACH ROW EXECUTE FUNCTION update_post_search_vector();
4. Erstellen eines Suchindex
Damit unsere Suche blitzschnell ist, besteht der endgültige Schritt darin, einen GIN-Index (Generalized Inverted Index) für unsere neue Spalte search_vector
zu erstellen.
CREATE INDEX post_search_vector_idx ON "post" USING gin(search_vector);
Jetzt ist Ihre Datenbank bereit! Sie wird automatisch einen effizienten Suchindex für jeden Artikel pflegen.
Schritt 2: Erstellen der Suchlogik in Nest.js
Nachdem die Datenbankschicht vorbereitet ist, kehren wir zu unserem Nest.js-Projekt zurück, um den Backend-Code für die Verarbeitung von Suchanfragen zu schreiben.
1. Aktualisieren von PostsService
Öffnen Sie src/posts/posts.service.ts
, wo wir eine neue search
-Methode hinzufügen müssen.
// src/posts/posts.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Post } from './post.entity'; @Injectable() export class PostsService { constructor( @InjectRepository(Post) private postsRepository: Repository<Post> ) {} // ... findAll, findOne, create Methoden bleiben unverändert async search(query: string): Promise<Post[]> { if (!query) { return []; } // Verwenden Sie QueryBuilder, um eine komplexere Abfrage zu erstellen return this.postsRepository .createQueryBuilder('post') .select() .addSelect("ts_rank(post.search_vector, to_tsquery('english', :query))", 'rank') .where("post.search_vector @@ to_tsquery('english', :query))", { query: `${query.split(' ').join(' & ')}` }) .orderBy('rank', 'DESC') .getMany(); } }
Code-Erklärung:
- Wir verwenden TypeORM's
QueryBuilder
, da er uns mehr Flexibilität für das Schreiben komplexer SQL-Abfragen bietet. to_tsquery('english', :query)
: Diese Funktion konvertiert die eingegebene Suchzeichenkette des Benutzers (z. B. "nestjs blog") in einen speziellen Abfragetyp, der mit einertsvector
-Spalte abgeglichen werden kann. Wir verbinden mehrere Wörter mit&
, um anzuzeigen, dass alle Wörter übereinstimmen müssen.- Der
@@
-Operator: Dies ist der "Match"-Operator für Volltextsuche. Die Zeilewhere("post.search_vector @@ ...")
ist der Kern der Suchoperation. ts_rank(...)
: Diese Funktion berechnet einen "Relevanz-Rang", basierend darauf, wie gut die Abfrageterme mit dem Text übereinstimmen..orderBy('rank', 'DESC')
: Wir sortieren nach diesem Rang in absteigender Reihenfolge, um sicherzustellen, dass die relevantesten Artikel ganz oben erscheinen.
2. Erstellen der Suchroute
Fügen Sie als Nächstes eine neue Route in src/posts/posts.controller.ts
hinzu, um Suchanfragen zu bearbeiten.
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Post, Body, Res, UseGuards, Request, Query } from '@nestjs/common'; // ... andere Importe @Controller() // Verschieben Sie 'posts' in einzelne Methoden export class PostsController { constructor(private readonly postsService: PostsService) {} @Get() // Wurzelpfad @Render('index') async root(@Request() req) { // ... } @Get('posts') // Startseiten-Weiterleitung @Render('index') async findAll(@Request() req) { const posts = await this.postsService.findAll(); return { posts, user: req.user }; } // Die neue Suchroute @Get('search') @Render('search-results') async search(@Query('q') query: string, @Request() req) { const posts = await this.postsService.search(query); return { posts, user: req.user, query }; } @UseGuards(AuthenticatedGuard) @Get('posts/new') // ... // ... andere Methoden }
Hinweis: Um die Route /search
zum Laufen zu bringen, haben wir die Struktur von PostsController
leicht angepasst. Wir haben @Controller('posts')
in @Controller()
geändert und den Routenpfad explizit für jede Methode angegeben, z. B. @Get('posts')
.
Schritt 3: Integration der Suchfunktion in das Frontend
Mit der bereitgestellten Backend-API fügen wir nun eine Suchleiste und eine Ergebnisseite zur Benutzeroberfläche hinzu.
1. Hinzufügen der Suchleiste
Öffnen Sie die Datei views/_header.ejs
und fügen Sie ein Suchformular zur Navigationsleiste hinzu.
<header> <h1><a href="/">My Blog</a></h1> <form action="/search" method="GET" class="search-form"> <input type="search" name="q" placeholder="Search posts..." /> <button type="submit">Search</button> </form> <div class="user-actions"> <% if (user) { %> <span>Welcome, User</span> <a href="/posts/new" class="new-post-btn">New Post</a> <a href="/auth/logout">Logout</a> <% } else { %> <a href="/auth/login">Login</a> <a href="/users/register">Register</a> <% } %> </div> </header>
2. Erstellen der Suchergebnisseite
Erstellen Sie eine neue Datei namens search-results.ejs
im Verzeichnis views
. Diese Seite wird verwendet, um die Suchergebnisse anzuzeigen.
<%- include('_header', { title: 'Search Results' }) %> <div class="search-results-container"> <h2>Search Results for: "<%= query %>"</h2> <% if (posts.length > 0) { %> <div class="post-list"> <% posts.forEach(post => { %> <article class="post-item"> <h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2> <p><%= post.content.substring(0, 150) %>...</p> <small><%= new Date(post.createdAt).toLocaleDateString() %></small> </article> <% }) %> </div> <% } else { %> <p>No posts found matching your search. Please try different keywords.</p> <% } %> </div> <%- include('_footer') %>
Diese Vorlage ist einfach: Sie zeigt zuerst die Suchanfrage des Benutzers an, prüft dann das posts
-Array. Wenn das Array nicht leer ist, durchläuft sie es und zeigt die Liste der Artikel an (genau wie auf der Startseite); wenn es leer ist, wird eine Meldung "Keine Beiträge gefunden" angezeigt.
Ausführen und Testen
Alles erledigt! Starten Sie nun Ihre Anwendung neu:
npm run start:dev
Öffnen Sie Ihren Browser, Sie sollten eine neue Suchleiste oben auf der Seite sehen.
- Geben Sie ein Wort ein, das in einem der Titel oder Inhalte Ihrer Beiträge vorkommt, und drücken Sie Enter.
- Die Seite sollte zur Suchergebnisseite navigieren und die entsprechenden Artikel anzeigen.
- Versuchen Sie, nach einem Wort zu suchen, das nicht existiert, und die Seite wird Sie freundlich informieren: "Keine Beiträge gefunden".
- Wenn einer Ihrer Artikel das Wort "creating" enthält, versuchen Sie nach "create" zu suchen, um zu sehen, ob die leistungsstarke Stemming-Funktion von PostgreSQL das Ergebnis korrekt abgleicht!
Ihr Blog unterstützt nun eine Volltextsuchfunktion. Egal, wie viel Sie schreiben, Ihre Leser werden sich nicht mehr verirren.