Implement MongoDB Full Text Search with Spring Data MongoDB.
Background
MongoDB full text search provides the flexibility to perform search entries through multiple fields. In this example we will explore how to implement full text search with Spring Data MongoDB.
Verification
Given we have the following entries in Character:
Name | Publisher |
---|---|
Captain Marvel |
Marvel |
Joker |
DC |
Thanos |
Marvel |
When searching for captain marvel
then the following results should be returned
Name | Publisher |
---|---|
Captain Marvel |
Marvel |
Thanos |
Marvel |
This is demonstrated in CharacterRepositoryTests.
Implementation
We will start by defining the Character
entity.
@Document
class Character {
@Id
private ObjectId id;
@TextIndexed
private final String name;
@TextIndexed
private final String publisher;
public Character(String name, String publisher) {
this.name = name;
this.publisher = publisher;
}
}
With Predefined Index
If respective fields are already indexed
then we can utilise Spring Data query generation to perform full text search.
This can be done by creating a method that takes TextCriteria
as parameter:
interface CharacterRepository extends MongoRepository<Character, ObjectId>, CharacterSearchRepository {
List<Character> findAllBy(TextCriteria criteria, Sort sort);
}
This method can then be used in the following manner:
@Testcontainers
@DataMongoTest
class CharacterRepositoryTests {
@Container
@ServiceConnection
private final static MongoDBContainer mongo = new MongoDBContainer("mongo:latest");
@Autowired
private MongoOperations operations;
@Autowired
private CharacterRepository repository;
@Test
@DisplayName("Generated query: Search for 'captain marvel' should return 'Captain Marvel' and 'Thanos'")
void withGeneratedQuery() {
// Simulate predefined index
operations.indexOps(Character.class).ensureIndex(new TextIndexDefinitionBuilder().onFields("name", "publisher").build());
var characters = repository.findAllBy(new TextCriteria().matchingAny("captain", "marvel"), Sort.by("name"));
assertThat(characters)
.hasSize(2)
.extracting("name")
.containsOnly("Captain Marvel", "Thanos")
.doesNotContain("Joker");
}
}
Without Predefined Index
Without predefined index, we will need to implement a custom repository implementation. We will start by defining a custom repository interface, CharacterSearchRepository:
interface CharacterSearchRepository {
List<Character> findByText(String text, Sort sort);
}
Next, implement the custom repository interface in CharacterSearchRepositoryImpl:
class CharacterSearchRepositoryImpl implements CharacterSearchRepository {
private final MongoOperations operations;
CharacterSearchRepositoryImpl(MongoOperations operations) {
this.operations = operations;
}
@Override
public List<Character> findByText(String text, Sort sort) {
operations.indexOps(Character.class)
.ensureIndex(new TextIndexDefinitionBuilder().onFields("name", "publisher").build());
var parameters = text.split(" ");
var query = TextQuery.queryText(new TextCriteria().matchingAny(parameters)).with(sort);
return operations.find(query, Character.class);
}
}
This implementation will indexed
searchable fields, i.e. name
and publisher
before searching the Document
.
Finally, we will verify our custom implementation through integration test:
@Testcontainers
@DataMongoTest
class CharacterRepositoryTests {
@Container
@ServiceConnection
private final static MongoDBContainer mongo = new MongoDBContainer("mongo:latest");
@Autowired
private MongoOperations operations;
@Autowired
private CharacterRepository repository;
@Test
@DisplayName("Custom implementation: Search for 'captain marvel' should return 'Captain Marvel' and 'Thanos'")
void findByText() {
var characters = repository.findByText("captain marvel", Sort.by("name"));
assertThat(characters)
.hasSize(2)
.extracting("name")
.containsOnly("Captain Marvel", "Thanos")
.doesNotContain("Joker");
}
}