Setup Spring and Postgres for Full Text Search

Fabio Hiroki - Jul 7 '20 - - Dev Community

Introduction

In this article we will build a Pokemon API capable of full text search. That means we will be able to search Pokemons by its text description with the power of Spring Boot and Postresql!

The greatest advantage of this setup is the use of JPA (Java Persistence API) for database interaction, instead of using native SQL queries directly. In another words, when we query database we will get Java objects (Entities) automatically.

You can try the frontend demo on CodeSandbox:

Backend code is on Github:

GitHub logo fabiothiroki / spring-pokemon-textsearch

A spring boot application implementing full-text search using Postgresql

Credits for frontend design goes to Florin Pop.

Setup

Go to Spring Initializr to select the basic dependencies and download the boilerplate of our project. At the time I am writing this article, I've selected:

  • Gradle Project
  • Java 8 Language
  • Spring Boot 2.3.1
  • Spring Web
  • Spring Data JPA

Spring configuration

Full text search

Before we start effectively coding, let's take a step back and talk about the use case for full text search on our API. Supposing I query the API for word growing, I want at least the following results:

  • Bulbasaur: For some time after its birth, it grows by gaining nourishment from the seed on its back.
  • Nidorina: When it senses danger, it raises all the barbs on its body. These barbs grow slower than Nidorinos.

We could achieve this by using the common SQL LIKE operator, but we would still need to know the stem (or the root form) of growing word. So the query would be:

SELECT * FROM Pokemon WHERE description LIKE 'grow%'
Enter fullscreen mode Exit fullscreen mode

But now for every other English word we would like to search, we would need to manually map the word to its corresponding stem. That's where full text search helps us so we can query by any word.

The same search above using the full text search operators from Postgresql is:

SELECT * FROM Pokemon 
WHERE to_tsvector(description) @@ plainto_tsquery('growing')
Enter fullscreen mode Exit fullscreen mode

As you can notice, we can query directly by word growing and get the same results. In the next section I will explain what to_tsvector and plainto_tsquery means.

to_tsvector

Basically it receives a string as input and returns a list of lexemes (a minimal meaningful unit of language). Let's check what are the lexemes of Bulbasaur description:

For some time after its birth, it grows by gaining nourishment from the seed on its back.

SELECT to_tsvector(description) FROM pokemon where id=1
---
'back':17 'birth':6 'gain':10 'grow':8 'nourish':11 'seed':14 'time':3
Enter fullscreen mode Exit fullscreen mode

As you can notice the grows word is reduced to its lexeme grow.

plainto_tsquery

This operator transforms a string to a tsquery, that is a list of tokens and Boolean operators describing the terms we would like to search.

SELECT plainto_tsquery('growing')
---
'grow'
Enter fullscreen mode Exit fullscreen mode

In short words, we apply to_tsvector on the database column containing the text we would like to match, and we apply plainto_tsquery on search input.

Custom operators on JPQL

So how do we use these cool text operators on Spring Boot without having to write native SQL queries directly?

First we have to declare a MetadataBuilderContributor class to register a new SQL function:

public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor {
    @Override
    public void contribute(MetadataBuilder metadataBuilder) {
        metadataBuilder.applySqlFunction("fts",
                new SQLFunctionTemplate(BooleanType.INSTANCE,
                        "to_tsvector(description) @@ plainto_tsquery(?1)"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Then on application.properties configuration file, we just need to add a reference to this class:

spring.jpa.properties.hibernate.metadata_builder_contributor=com.example.demo.fts.SqlFunctionsMetadataBuilderContributor
Enter fullscreen mode Exit fullscreen mode

Here we're registering an fts function that receives one parameter and tries to match the parameter with description column from Pokemon table.

Repository

Now on repository class, we can use the fts operator directly on JPQL:

@Repository
public interface PokemonRepository extends CrudRepository<Pokemon, Long> {

    @Query("SELECT p FROM Pokemon p WHERE fts(:description) = true")
    List<Pokemon> search(@Param("description") String description);

}
Enter fullscreen mode Exit fullscreen mode

Controller

Finally we implement our API that receives a search parameter on URL and call the respective method from its repository:

@RestController()
@RequestMapping("/pokemon")
public class PokemonController {

  @Autowired
  private PokemonRepository repository;

  @GetMapping()
  public List<Pokemon> findByDescription(@RequestParam String search) {
      return repository.search(search);
  }
}
Enter fullscreen mode Exit fullscreen mode

You can test the endpoint by accessing http://localhost:8080/pokemon/?search=growing on you browser.

Conclusion

We learned how we can use JPA to power up full text search search queries. It can be very helpful on catching errors on compilation time while taking a lot of performance advantage on execution time.

. . . . . . . . . . . . . . . . . . . . . . . . . . . .