Volltextsuche mit PostgreSQL und Spring (Teil II)

Nachdem wir uns im ersten Teil mit den Grundlagen beschäftigt haben, wollen wir die Volltextsuche nun auch in einer kleinen real-world Application nutzen.

Repository

Die Integration mit Spring Data JPA ist denkbar einfach. Da wir PostgreSQL spezifische SQL Statements verwenden, schreiben wir ein natives Query. Spring Data JPA erzeugt daraus wie gewohnt unsere Objekte.

@Query(
    value = "SELECT * " +
            "FROM articles " +
            "WHERE ts @@ plainto_tsquery('german',:query)", nativeQuery = true
)
fun search(query: String): List<Article>

Wollen wir die Top 10 Ergebnisse, wird das Query etwas umfangreicher, bleibt aber dennoch übersichtlich und die verwendete Technologie bleibt gegenüber unserer Domäne verborgen:

@Query(
    value = "SELECT content, ts_rank_cd(ts, query) AS rank " +
            "FROM articles, plainto_tsquery('german',:query) query " +
            "WHERE query @@ ts " +
            "ORDER BY rank DESC " +
            "LIMIT 10;", nativeQuery = true
)
fun searchTop10(query: String): List<Article>

Um damit etwas herumspielen zu können, eignet sich die Beispielanwendung hier im GitHub Repository oder wir schreiben uns schnell ein paar Integrationstests (wie das für jedes gute Softwareprojekt selbstverständlich sein sollte).

Integration Test

Für solche Tests hat sich in der Praxis Testcontainers bewährt.

Wir können hier direkt den passenden PostgreSQL Container nutzen, den uns Testcontainers zur Verfügung stellt. Dadurch müssen wir uns keine Gedanken machen, wie wir die Datenbank im Container richtig konfigurieren und starten. Zudem kann einfach auf die benötigten Variablen zugegriffen werden (z.B. Username, Password und JDBC URL).

companion object {
    val postgres = PostgreSQLContainer("postgres:16.0")

    @JvmStatic
    @BeforeAll
    fun startPostgres() = postgres.start()

    @JvmStatic
    @AfterAll
    fun stopPostgres() = postgres.stop()
//...

Um die entsprechenden Properties für Spring zu setzen, registrieren wir hierfür noch eine eigene PropertySource:

    @JvmStatic
    @DynamicPropertySource
    fun registerDBContainer(registry: DynamicPropertyRegistry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl)
        registry.add("spring.datasource.username", postgres::getUsername)
        registry.add("spring.datasource.password", postgres::getPassword)
    }
}

Das ganze lässt sich ganz gut in einer IntegrationTest Klasse kapseln, von der jeder Integrationstest erbt.

Im entsprechenden Test müssen wir dann mit folgender Annotation verhindern, dass unsere Datenbank ersetzt wird (z.B. durch eine H2-InMemory)

@AutoConfigureTestDatabase(replace = NONE)

Nun können wir in einem kleinen Test schauen, ob unsere Artikel gefunden werden.

@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
class ArticleRepositoryIT(@Autowired private val articleRepository: ArticleRepository) : IntegrationTest() {


    @Test
    fun `can search articles by title`() {
        articleRepository.saveAll(ArticleFixture.ALL_EXAMPLE_ARTICLES)

        val articles = articleRepository.search("Datenschutz")

        assertThat(articles).hasSize(1)
        assertThat(articles.first().titel).isEqualTo(ArticleFixture.EXAMPLE_ARTICLE_3.titel)
    }

Fazit

Schon mit PostgreSQL und Spring Data JPA Bordmitteln ist es vergleichsweise einfach eine Volltextsuche umzusetzen, um flexibel über den Inhalt mehrere Spalten zu suchen. Mit der mächtigen Query Syntax, mit der z.B. auch noch mit unterschiedlichen Gewichtungen gearbeitet werden kann, sind auch komplexe Abfragen möglich. Gerade für kleinere Projekte wird dies oft die wirtschaftlichere Lösung sein und man vermeidet, gleich mit Kanonen auf Spatzen zu schießen.

Alles zusammen findet man hier im GitHub Repository.

Kommentare

Vielen Dank für Deinen Kommentar! Dieser muss jetzt nur noch freigegeben werden.

Ich akzeptiere die Kommentarrichtlinien sowie die Datenschutzbestimmungen* *Pflichtfelder