#8 - Add MongoDB text search example.

The sample reads the Spring IO blog Atom feed and performs text search on it.

We use manual index creation and query via MongoTemplate as well as automatic index creation and derived queries via repositories.

Original pull request: #10.
This commit is contained in:
Christoph Strobl
2014-08-27 14:53:29 +02:00
committed by Oliver Gierke
parent 4246700be0
commit 09317bc6de
12 changed files with 602 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ We have separate folders for the samples of individual modules:
* `geo-spatial` - Example project for general repository functionality (including geo-spatial functionality) and Querydsl integration
* `aggregation` - Example project to showcase the MongoDB aggregation framework support.
* `text-search` - Example project showing usage of MongoDB text search feature.
## Spring Data REST

View File

@@ -19,6 +19,7 @@
<modules>
<module>geo-spatial</module>
<module>aggregation</module>
<module>text-search</module>
</modules>
<dependencies>

View File

@@ -0,0 +1,32 @@
# Spring Data MongoDB - Text Search Examples
This project contains samples of text search specific features of Spring Data Mongodb.
## Support for Text Index
Define text index structures manually (like below) or use `@TextIndexed` to mark content to be indexed for full text search.
```java
TextIndexDefinition textIndex = new TextIndexDefinitionBuilder()
.onField("title", 3F)
.onField("content", 2F)
.onField("categories")
.build();
template.indexOps(BlogPost.class).ensureIndex(textIndex);
```
## Support for full text repository queries
Use derived finder methods to search for terms and phrases.
```java
interface BlogPostRepository extends CrudRepository<BlogPost, String> {
// page through results for full text query
Page<BlogPost> findBy(TextCriteria criteria, Pageable page);
// find all matching documents and sort by relevance
List<BlogPost> findAllByOrderByScoreDesc(TextCriteria criteria);
}
```

View File

@@ -0,0 +1,26 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-mongodb-text-search</artifactId>
<name>Spring Data MongoDB - Text Search</name>
<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-examples</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.java.dev.rome</groupId>
<artifactId>rome</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.textsearch;
import java.util.List;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.TextIndexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.TextScore;
/**
* @author Christoph Strobl
*/
@Document
public class BlogPost {
private @Id String id;
private @TextIndexed(weight = 3) String title;
private @TextIndexed(weight = 2) String content;
private @TextIndexed List<String> categories;
private @TextScore Float score;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public List<String> getCategories() {
return categories;
}
public void setCategories(List<String> categories) {
this.categories = categories;
}
public Float getScore() {
return score;
}
public void setScore(Float score) {
this.score = score;
}
@Override
public String toString() {
return "BlogPost [score=" + score + ", id=" + id + ", title=" + title + ", categories=" + getCategories() + "]";
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.textsearch;
import java.util.List;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.CrudRepository;
/**
* @author Christoph Strobl
*/
public interface BlogPostRepository extends CrudRepository<BlogPost, String> {
List<BlogPost> findAllBy(TextCriteria criteria);
List<BlogPost> findAllByOrderByScoreDesc(TextCriteria criteria);
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.textsearch;
import javax.annotation.PreDestroy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import example.springdata.mongodb.util.BlogPostInitializer;
/**
* @author Christoph Strobl
*/
@Configuration
@EnableMongoRepositories
public class MongoTestConfiguration extends AbstractMongoConfiguration {
static final String DATABASE_NAME = "s2gx2014-blog";
static final String BLOG_POST_ATOM_FEED_SOURCE = "https://spring.io/blog.atom";
@Override
protected String getDatabaseName() {
return DATABASE_NAME;
}
@Override
public Mongo mongo() throws Exception {
return new MongoClient();
}
/**
* Initializes the repository with a predefined set of entities.
*
* @return
*/
@Bean
public BlogPostInitializer initializer() {
return new BlogPostInitializer(BLOG_POST_ATOM_FEED_SOURCE);
}
/**
* Clean up after execution by dropping used test db instance.
*
* @throws Exception
*/
@PreDestroy
public void dropTestDB() throws Exception {
mongo().dropDatabase(getDatabaseName());
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.textsearch;
import static example.springdata.mongodb.util.ConsoleResultPrinter.*;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { MongoTestConfiguration.class })
public class TextSearchRepositoryTests {
@Autowired BlogPostRepository repo;
/**
* Show how to do simple matching. <br />
* Note that text search is case insensitive and will also find entries like {@literal releases}.
*/
@Test
public void findAllBlogPostsWithRelease() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("release");
List<BlogPost> blogPosts = repo.findAllBy(criteria);
printResult(blogPosts, criteria);
}
/**
* Simple matching using negation.
*/
@Test
public void findAllBlogPostsWithReleaseButHeyIDoWantTheEngineeringStuff() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("release").notMatching("engineering");
List<BlogPost> blogPosts = repo.findAllBy(criteria);
printResult(blogPosts, criteria);
}
/**
* Phrase matching looks for the whole phrase as one.
*/
@Test
public void findAllBlogPostsByPhrase() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingPhrase("release candidate");
List<BlogPost> blogPosts = repo.findAllBy(criteria);
printResult(blogPosts, criteria);
}
/**
* Sort by relevance relying on the value marked with {@link org.springframework.data.mongodb.core.mapping.TextScore}.
*/
@Test
public void findAllBlogPostsByPhraseSortByScore() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingPhrase("release candidate");
List<BlogPost> blogPosts = repo.findAllByOrderByScoreDesc(criteria);
printResult(blogPosts, criteria);
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.textsearch;
import static example.springdata.mongodb.util.ConsoleResultPrinter.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.TextIndexDefinition;
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.core.query.TextQuery;
import com.mongodb.MongoClient;
import example.springdata.mongodb.util.BlogPostInitializer;
/**
* @author Christoph Strobl
*/
public class TextSearchTemplateTests {
MongoTemplate template;
@Before
public void setUp() throws Exception {
template = new MongoTemplate(new MongoClient(), MongoTestConfiguration.DATABASE_NAME);
template.dropCollection(BlogPost.class);
createIndex();
loadTestData();
}
/**
* Show how to do simple matching. <br />
* Note that text search is case insensitive and will also find entries like {@literal releases}.
*/
@Test
public void findAllBlogPostsWithRelease() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("release");
List<BlogPost> blogPosts = template.find(query(criteria), BlogPost.class);
printResult(blogPosts, criteria);
}
/**
* Sort by relevance relying on the value marked with {@link org.springframework.data.mongodb.core.mapping.TextScore}.
*/
@Test
public void findAllBlogPostsByPhraseSortByScore() {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingPhrase("release");
TextQuery query = new TextQuery(criteria);
query.setScoreFieldName("score");
query.sortByScore();
List<BlogPost> blogPosts = template.find(query, BlogPost.class);
printResult(blogPosts, criteria);
}
/**
* Creates the mongodb text index for {@link BlogPost}. <br />
*
* <pre>
* <code>
* db.collection.ensureIndex(
* {
* "title" : "text"
* "content" : "text"
* "categories" : "text",
* },
* {
* weights : {
* "title" : 3,
* "content" : 2
* }
* }
* )
* </code>
* </pre>
*/
private void createIndex() {
TextIndexDefinition textIndex = new TextIndexDefinitionBuilder()//
.onField("title", 3F) //
.onField("content", 2F) //
.onField("categories") //
.build();
template.indexOps(BlogPost.class).ensureIndex(textIndex);
}
private void loadTestData() throws Exception {
BlogPostInitializer initializer = new BlogPostInitializer(MongoTestConfiguration.BLOG_POST_ATOM_FEED_SOURCE);
initializer.initialize(this.template);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.util;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import com.sun.syndication.feed.atom.Category;
import com.sun.syndication.feed.atom.Content;
import com.sun.syndication.feed.atom.Entry;
import com.sun.syndication.feed.atom.Feed;
import example.springdata.mongodb.textsearch.BlogPost;
/**
* @author Christoph Strobl
*/
public class BlogPostInitializer implements InitializingBean {
private final String url;
private final RestTemplate restTemplate;
private final Converter<Entry, BlogPost> converter;
@Autowired MongoTemplate mongoTemplate;
public BlogPostInitializer(String url) {
restTemplate = new RestTemplate();
this.converter = new EntryConverter();
this.url = url;
}
public void initialize(MongoTemplate mongoTemplate) {
ResponseEntity<Feed> feed = restTemplate.getForEntity(url, Feed.class);
if (feed.hasBody()) {
for (Object entry : feed.getBody().getEntries()) {
if (entry instanceof Entry) {
mongoTemplate.save(converter.convert((Entry) entry));
}
}
}
}
@Override
public void afterPropertiesSet() throws Exception {
initialize(this.mongoTemplate);
}
/**
* {@link Converter} implementation capable of converting atom feed {@link Entry} into {@link BlogPost}.
*
* @author Christoph Strobl
*/
static class EntryConverter implements Converter<Entry, BlogPost> {
@Override
public BlogPost convert(Entry source) {
BlogPost post = new BlogPost();
post.setId(source.getId());
post.setTitle(source.getTitle());
for (Object content : source.getContents()) {
if (content instanceof Content) {
post.setContent(((Content) content).getValue());
}
}
List<String> categories = new ArrayList<String>();
for (Object category : source.getCategories()) {
if (category instanceof Category) {
categories.add(((Category) category).getLabel());
}
}
post.setCategories(categories);
return post;
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.mongodb.util;
import java.util.Collection;
import org.springframework.data.mongodb.core.query.TextCriteria;
import example.springdata.mongodb.textsearch.BlogPost;
/**
* Just a little helper for showing {@link BlogPost}s output on the console.
*
* @author Christoph Strobl
*/
public class ConsoleResultPrinter {
public static void printResult(Collection<BlogPost> blogPosts, TextCriteria criteria) {
System.out.println(String.format("XXXXXXXXXXXX -- Found %s blogPosts matching '%s' --XXXXXXXXXXXX",
blogPosts.size(), criteria != null ? criteria.getCriteriaObject() : ""));
for (BlogPost blogPost : blogPosts) {
System.out.println(blogPost);
}
System.out.println("XXXXXXXXXXXX -- XXXXXXXXXXXX -- XXXXXXXXXXXX\r\n");
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<root level="warn">
<appender-ref ref="console" />
</root>
</configuration>