DATAMONGO-973 - Add support for deriving full-text queries.

Added support to execute full-text queries on repositories. Query methods now can have a parameter of type TextCriteria which will be triggering a text search clause for the property annotated with @TestScore.

Retrieving document score and sorting by score is only possible if the entity holds a property annotated with @TextScore. If present, any find execution will be enriched so that it asserts loading of the according { $meta : textScore } field. The sort object will only be mapped in case the existing sort property already exists - in that case we replace the existing expression for the property with its $meta representation.

This allows for example the following:

TextCriteria criteria = TextCriteria.forDefaultLanguage().matching("term");

repository.findAllBy(criteria, new Sort("score"));
repository.findAllBy(criteria, new PageRequest(0, 10, Direction.DESC, "score"));
repository.findByFooOrderByScoreDesc("foo", criteria);

For more details and examples see the "Full text search queries" section in the reference manual.
This commit is contained in:
Christoph Strobl
2014-07-04 21:29:33 +02:00
committed by Oliver Gierke
parent 168cf3e1f6
commit 151b1d4510
39 changed files with 1007 additions and 154 deletions

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<context version="7.1.9.205">
<context version="7.1.10.209">
<scope type="Project" name="spring-data-mongodb">
<element type="TypeFilterReferenceOverridden" name="Filter">
<element type="IncludeTypePattern" name="org.springframework.data.mongodb.**"/>
@@ -32,6 +32,7 @@
<element type="IncludeTypePattern" name="**.config.**"/>
</element>
<dependency toName="Project|spring-data-mongodb::Layer|Core::Subsystem|Mapping" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|API" type="AllowedDependency"/>
<dependency toName="Project|spring-data-mongodb::Layer|Repositories::Subsystem|Implementation" type="AllowedDependency"/>
</element>
<dependency toName="Project|spring-data-mongodb::Layer|Config" type="AllowedDependency"/>

View File

@@ -1616,7 +1616,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
CursorPreparer preparer, DbObjectCallback<T> objectCallback) {
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
DBObject mappedFields = fields == null ? null : queryMapper.getMappedObject(fields, entity);
DBObject mappedFields = queryMapper.getMappedFields(fields, entity);
DBObject mappedQuery = queryMapper.getMappedObject(query, entity);
if (LOGGER.isDebugEnabled()) {
@@ -1969,8 +1970,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
return null;
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(type);
return queryMapper.getMappedObject(query.getSortObject(), entity);
return queryMapper.getMappedSort(query.getSortObject(), mappingContext.getPersistentEntity(type));
}
// Callback implementations
@@ -2030,7 +2030,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
}
public DBCursor doInCollection(DBCollection collection) throws MongoException, DataAccessException {
if (fields == null) {
if (fields == null || fields.toMap().isEmpty()) {
return collection.find(query);
} else {
return collection.find(query, fields);

View File

@@ -45,6 +45,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToS
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToURLConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.TermToStringConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.URLToStringConverter;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.util.Assert;
@@ -58,6 +59,7 @@ import org.springframework.util.Assert;
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class CustomConversions {
@@ -106,7 +108,8 @@ public class CustomConversions {
toRegister.add(URLToStringConverter.INSTANCE);
toRegister.add(StringToURLConverter.INSTANCE);
toRegister.add(DBObjectToStringConverter.INSTANCE);
toRegister.add(TermToStringConverter.INSTANCE);
toRegister.addAll(JodaTimeConverters.getConvertersToRegister());
toRegister.addAll(GeoConverters.getConvertersToRegister());

View File

@@ -25,6 +25,8 @@ import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.core.query.Term;
import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
@@ -34,6 +36,7 @@ import com.mongodb.DBObject;
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
abstract class MongoConverters {
@@ -160,4 +163,19 @@ abstract class MongoConverters {
return source == null ? null : source.toString();
}
}
/**
* @author Christoph Strobl
* @since 1.6
*/
@WritingConverter
public static enum TermToStringConverter implements Converter<Term, String> {
INSTANCE;
@Override
public String convert(Term source) {
return source == null ? null : source.getFormatted();
}
}
}

View File

@@ -57,6 +57,11 @@ import com.mongodb.DBRef;
public class QueryMapper {
private static final List<String> DEFAULT_ID_NAMES = Arrays.asList("id", "_id");
private static final DBObject META_TEXT_SCORE = new BasicDBObject("$meta", "textScore");
private enum MetaMapping {
FORCE, WHEN_PRESENT, IGNORE;
}
private final ConversionService conversionService;
private final MongoConverter converter;
@@ -119,6 +124,61 @@ public class QueryMapper {
return result;
}
/**
* Maps fields used for sorting to the {@link MongoPersistentEntity}s properties. <br />
* Also converts properties to their {@code $meta} representation if present.
*
* @param sortObject
* @param entity
* @return
* @since 1.6
*/
public DBObject getMappedSort(DBObject sortObject, MongoPersistentEntity<?> entity) {
if (sortObject == null) {
return null;
}
DBObject mappedSort = getMappedObject(sortObject, entity);
mapMetaAttributes(mappedSort, entity, MetaMapping.WHEN_PRESENT);
return mappedSort;
}
/**
* Maps fields to retrieve to the {@link MongoPersistentEntity}s properties. <br />
* Also onverts and potentially adds missing property {@code $meta} representation.
*
* @param fieldsObject
* @param entity
* @return
* @since 1.6
*/
public DBObject getMappedFields(DBObject fieldsObject, MongoPersistentEntity<?> entity) {
DBObject mappedFields = fieldsObject != null ? getMappedObject(fieldsObject, entity) : new BasicDBObject();
mapMetaAttributes(mappedFields, entity, MetaMapping.FORCE);
return mappedFields.keySet().isEmpty() ? null : mappedFields;
}
private void mapMetaAttributes(DBObject source, MongoPersistentEntity<?> entity, MetaMapping metaMapping) {
if (entity == null || source == null) {
return;
}
if (entity.hasTextScoreProperty() && !MetaMapping.IGNORE.equals(metaMapping)) {
MongoPersistentProperty textScoreProperty = entity.getTextScoreProperty();
if (MetaMapping.FORCE.equals(metaMapping)
|| (MetaMapping.WHEN_PRESENT.equals(metaMapping) && source.containsField(textScoreProperty.getFieldName()))) {
source.putAll(getMappedTextScoreField(textScoreProperty));
}
}
}
private DBObject getMappedTextScoreField(MongoPersistentProperty property) {
return new BasicDBObject(property.getFieldName(), META_TEXT_SCORE);
}
/**
* Extracts the mapped object value for given field out of rawValue taking nested {@link Keyword}s into account
*

View File

@@ -276,7 +276,7 @@ public class TextIndexDefinition implements IndexDefinition {
* @return
*/
public TextIndexDefinitionBuilder onField(String fieldname) {
return onField(fieldname, Float.NaN);
return onField(fieldname, 1F);
}
/**

View File

@@ -114,6 +114,24 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
return this.language;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#getTextScoreProperty()
*/
@Override
public MongoPersistentProperty getTextScoreProperty() {
return getPersistentProperty(TextScore.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.mapping.MongoPersistentEntity#hasTextScoreProperty()
*/
@Override
public boolean hasTextScoreProperty() {
return getTextScoreProperty() != null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.BasicPersistentEntity#verify()

View File

@@ -40,4 +40,21 @@ public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersi
*/
String getLanguage();
/**
* Returns the property holding text score value.
*
* @since 1.6
* @see #hasTextScoreProperty()
* @return {@literal null} if not present.
*/
MongoPersistentProperty getTextScoreProperty();
/**
* Returns whether the entity has a {@link TextScore} property.
*
* @since 1.6
* @return true if property annotated with {@link TextScore} is present.
*/
boolean hasTextScoreProperty();
}

View File

@@ -50,8 +50,12 @@ public class BasicQuery extends Query {
this.fieldsObject = fieldsObject;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.Query#addCriteria(org.springframework.data.mongodb.core.query.CriteriaDefinition)
*/
@Override
public Query addCriteria(Criteria criteria) {
public Query addCriteria(CriteriaDefinition criteria) {
this.queryObject.putAll(criteria.getCriteriaObject());
return this;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2011 the original author or authors.
* Copyright 2010-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.
@@ -17,8 +17,25 @@ package org.springframework.data.mongodb.core.query;
import com.mongodb.DBObject;
/**
* @author Oliver Gierke
* @author Christoph Strobl
*/
public interface CriteriaDefinition {
/**
* Get {@link DBObject} representation.
*
* @return
*/
DBObject getCriteriaObject();
}
/**
* Get the identifying {@literal key}.
*
* @return
* @since 1.6
*/
String getKey();
}

View File

@@ -343,7 +343,6 @@ public final class NearQuery {
*
* @return
*/
@SuppressWarnings("deprecation")
public DBObject toDBObject() {
BasicDBObject dbObject = new BasicDBObject();

View File

@@ -39,13 +39,14 @@ import com.mongodb.DBObject;
* @author Thomas Risberg
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
public class Query {
private static final String RESTRICTED_TYPES_KEY = "_$RESTRICTED_TYPES";
private final Set<Class<?>> restrictedTypes = new HashSet<Class<?>>();
private final Map<String, Criteria> criteria = new LinkedHashMap<String, Criteria>();
private final Map<String, CriteriaDefinition> criteria = new LinkedHashMap<String, CriteriaDefinition>();
private Field fieldSpec;
private Sort sort;
private int skip;
@@ -53,42 +54,48 @@ public class Query {
private String hint;
/**
* Static factory method to create a {@link Query} using the provided {@link Criteria}.
* Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}.
*
* @param criteria must not be {@literal null}.
* @param criteriaDefinition must not be {@literal null}.
* @return
* @since 1.6
*/
public static Query query(Criteria criteria) {
return new Query(criteria);
public static Query query(CriteriaDefinition criteriaDefinition) {
return new Query(criteriaDefinition);
}
public Query() {}
/**
* Creates a new {@link Query} using the given {@link Criteria}.
* Creates a new {@link Query} using the given {@link CriteriaDefinition}.
*
* @param criteria must not be {@literal null}.
* @param criteriaDefinition must not be {@literal null}.
* @since 1.6
*/
public Query(Criteria criteria) {
addCriteria(criteria);
public Query(CriteriaDefinition criteriaDefinition) {
addCriteria(criteriaDefinition);
}
/**
* Adds the given {@link Criteria} to the current {@link Query}.
* Adds the given {@link CriteriaDefinition} to the current {@link Query}.
*
* @param criteria must not be {@literal null}.
* @param criteriaDefinition must not be {@literal null}.
* @return
* @since 1.6
*/
public Query addCriteria(Criteria criteria) {
CriteriaDefinition existing = this.criteria.get(criteria.getKey());
String key = criteria.getKey();
public Query addCriteria(CriteriaDefinition criteriaDefinition) {
CriteriaDefinition existing = this.criteria.get(criteriaDefinition.getKey());
String key = criteriaDefinition.getKey();
if (existing == null) {
this.criteria.put(key, criteria);
this.criteria.put(key, criteriaDefinition);
} else {
throw new InvalidMongoDbApiUsageException("Due to limitations of the com.mongodb.BasicDBObject, "
+ "you can't add a second '" + key + "' criteria. " + "Query already contains '"
+ existing.getCriteriaObject() + "'.");
}
return this;
}
@@ -268,8 +275,8 @@ public class Query {
return hint;
}
protected List<Criteria> getCriteria() {
return new ArrayList<Criteria>(this.criteria.values());
protected List<CriteriaDefinition> getCriteria() {
return new ArrayList<CriteriaDefinition>(this.criteria.values());
}
/*

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
/**
* A {@link Term} defines one or multiple words {@link Type#WORD} or phrases {@link Type#PHRASE} to be used in the
@@ -24,7 +24,7 @@ package org.springframework.data.mongodb.core.query.text;
*/
public class Term {
enum Type {
public enum Type {
WORD, PHRASE;
}

View File

@@ -13,16 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
@@ -30,42 +26,59 @@ import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
/**
* Implementation of {@link CriteriaDefinition} to be used for full text search .
* Implementation of {@link CriteriaDefinition} to be used for full text search.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.6
*/
public class TextCriteria extends Criteria {
public class TextCriteria implements CriteriaDefinition {
private final List<Term> terms;
private String language;
private List<Term> terms;
/**
* Creates a new {@link TextCriteria}.
*
* @see #forDefaultLanguage()
* @see #forLanguage(String)
*/
public TextCriteria() {
this(null);
}
private TextCriteria(String language) {
this.language = language;
this.terms = new ArrayList<Term>();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getCriteriaObject()
/**
* Returns a new {@link TextCriteria} for the default language.
*
* @return
*/
@Override
public DBObject getCriteriaObject() {
BasicDBObjectBuilder builder = new BasicDBObjectBuilder();
if (StringUtils.hasText(language)) {
builder.add("$language", language);
}
if (!CollectionUtils.isEmpty(terms)) {
builder.add("$search", join(terms.iterator()));
}
return new BasicDBObject("$text", builder.get());
public static TextCriteria forDefaultLanguage() {
return new TextCriteria();
}
/**
* @param words
* For a full list of supported languages see the mongdodb reference manual for <a
* href="http://docs.mongodb.org/manual/reference/text-search-languages/">Text Search Languages</a>.
*
* @param language
* @return
*/
public static TextCriteria forLanguage(String language) {
Assert.hasText(language, "Language must not be null or empty!");
return new TextCriteria(language);
}
/**
* Configures the {@link TextCriteria} to match any of the given words.
*
* @param words the words to match.
* @return
*/
public TextCriteria matchingAny(String... words) {
@@ -73,22 +86,21 @@ public class TextCriteria extends Criteria {
for (String word : words) {
matching(word);
}
return this;
}
/**
* Add given {@link Term} to criteria.
* Adds given {@link Term} to criteria.
*
* @param term must not be null.
* @param term must not be {@literal null}.
*/
public void matching(Term term) {
public TextCriteria matching(Term term) {
Assert.notNull(term, "Term to add must not be null.");
this.terms.add(term);
}
private void notMatching(Term term) {
matching(term.negate());
this.terms.add(term);
return this;
}
/**
@@ -110,7 +122,7 @@ public class TextCriteria extends Criteria {
public TextCriteria notMatching(String term) {
if (StringUtils.hasText(term)) {
notMatching(new Term(term, Term.Type.WORD));
matching(new Term(term, Term.Type.WORD).negate());
}
return this;
}
@@ -136,7 +148,7 @@ public class TextCriteria extends Criteria {
public TextCriteria notMatchingPhrase(String phrase) {
if (StringUtils.hasText(phrase)) {
notMatching(new Term(phrase, Term.Type.PHRASE));
matching(new Term(phrase, Term.Type.PHRASE).negate());
}
return this;
}
@@ -155,69 +167,45 @@ public class TextCriteria extends Criteria {
return this;
}
/**
* @return
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getKey()
*/
public static TextCriteria forDefaultLanguage() {
return new TextCriteriaBuilder().build();
}
/**
* For a full list of supported languages see the mongdodb reference manual for <a
* href="http://docs.mongodb.org/manual/reference/text-search-languages/">Text Search Languages</a>.
*
* @param language
* @return
*/
public static TextCriteria forLanguage(String language) {
return new TextCriteriaBuilder().withLanguage(language).build();
}
private static String join(Iterator<Term> iterator) {
Term first = iterator.next();
if (!iterator.hasNext()) {
return first.getFormatted();
}
StringBuilder buf = new StringBuilder(256);
if (first != null) {
buf.append(first);
}
while (iterator.hasNext()) {
buf.append(' ');
Term obj = iterator.next();
if (obj != null) {
buf.append(obj.getFormatted());
}
}
return buf.toString();
}
public static class TextCriteriaBuilder {
private TextCriteria instance;
public TextCriteriaBuilder() {
this.instance = new TextCriteria();
}
public TextCriteriaBuilder withLanguage(String language) {
this.instance.language = language;
return this;
}
public TextCriteria build() {
return this.instance;
}
}
@Override
public String getKey() {
return "$text";
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getCriteriaObject()
*/
@Override
public DBObject getCriteriaObject() {
BasicDBObjectBuilder builder = new BasicDBObjectBuilder();
if (StringUtils.hasText(language)) {
builder.add("$language", language);
}
if (!terms.isEmpty()) {
builder.add("$search", join(terms));
}
return new BasicDBObject("$text", builder.get());
}
private String join(Iterable<Term> terms) {
List<String> result = new ArrayList<String>();
for (Term term : terms) {
if (term != null) {
result.add(term.getFormatted());
}
}
return StringUtils.collectionToDelimitedString(result, " ");
}
}

View File

@@ -13,12 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
import java.util.Locale;
import org.springframework.data.mongodb.core.query.Query;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
@@ -147,6 +145,10 @@ public class TextQuery extends Query {
return scoreFieldName;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.Query#getFieldsObject()
*/
@Override
public DBObject getFieldsObject() {
@@ -155,24 +157,32 @@ public class TextQuery extends Query {
}
DBObject fields = super.getFieldsObject();
if (fields == null) {
fields = new BasicDBObject();
}
fields.put(getScoreFieldName(), META_TEXT_SCORE);
return fields;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.Query#getSortObject()
*/
@Override
public DBObject getSortObject() {
DBObject sort = new BasicDBObject();
if (this.sortByScore) {
sort.put(getScoreFieldName(), META_TEXT_SCORE);
}
if (super.getSortObject() != null) {
sort.putAll(super.getSortObject());
}
return sort;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2012 the original author or authors.
* Copyright 2010-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.
@@ -26,6 +26,7 @@ import org.springframework.data.repository.PagingAndSortingRepository;
* Mongo specific {@link org.springframework.data.repository.Repository} interface.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
@NoRepositoryBean
public interface MongoRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-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.
@@ -27,6 +27,7 @@ import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@@ -38,6 +39,7 @@ import com.mongodb.DBRef;
* Custom {@link ParameterAccessor} that uses a {@link MongoWriter} to serialize parameters into Mongo format.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class ConvertingParameterAccessor implements MongoParameterAccessor {
@@ -110,6 +112,14 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor {
return delegate.getGeoNearLocation();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getFullText()
*/
public TextCriteria getFullText() {
return delegate.getFullText();
}
/**
* Converts the given value with the underlying {@link MongoWriter}.
*

View File

@@ -17,12 +17,14 @@ package org.springframework.data.mongodb.repository.query;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParameterAccessor;
/**
* Mongo-specific {@link ParameterAccessor} exposing a maximum distance parameter.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public interface MongoParameterAccessor extends ParameterAccessor {
@@ -40,4 +42,12 @@ public interface MongoParameterAccessor extends ParameterAccessor {
* @return
*/
Point getGeoNearLocation();
/**
* Returns the {@link TextCriteria} to be used for full text query.
*
* @return null if not set.
* @since 1.6
*/
TextCriteria getFullText();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-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.
@@ -22,6 +22,7 @@ import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.Near;
import org.springframework.data.mongodb.repository.query.MongoParameters.MongoParameter;
import org.springframework.data.repository.query.Parameter;
@@ -31,10 +32,13 @@ import org.springframework.data.repository.query.Parameters;
* Custom extension of {@link Parameters} discovering additional
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class MongoParameters extends Parameters<MongoParameters, MongoParameter> {
private final Integer distanceIndex;
private final Integer fullTextIndex;
private Integer nearIndex;
/**
@@ -48,6 +52,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
super(method);
List<Class<?>> parameterTypes = Arrays.asList(method.getParameterTypes());
this.distanceIndex = parameterTypes.indexOf(Distance.class);
this.fullTextIndex = parameterTypes.indexOf(TextCriteria.class);
if (this.nearIndex == null && isGeoNearMethod) {
this.nearIndex = getNearIndex(parameterTypes);
@@ -56,12 +61,14 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
}
}
private MongoParameters(List<MongoParameter> parameters, Integer distanceIndex, Integer nearIndex) {
private MongoParameters(List<MongoParameter> parameters, Integer distanceIndex, Integer nearIndex,
Integer fullTextIndex) {
super(parameters);
this.distanceIndex = distanceIndex;
this.nearIndex = nearIndex;
this.fullTextIndex = fullTextIndex;
}
@SuppressWarnings({ "unchecked", "deprecation" })
@@ -124,13 +131,31 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
return nearIndex;
}
/**
* Returns ths inde of the parameter to be used as a textquery param
*
* @return
* @since 1.6
*/
public int getFullTextParameterIndex() {
return fullTextIndex != null ? fullTextIndex.intValue() : -1;
}
/**
* @return
* @since 1.6
*/
public boolean hasFullTextParameter() {
return this.fullTextIndex != null && this.fullTextIndex.intValue() >= 0;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List)
*/
@Override
protected MongoParameters createFrom(List<MongoParameter> parameters) {
return new MongoParameters(parameters, this.distanceIndex, this.nearIndex);
return new MongoParameters(parameters, this.distanceIndex, this.nearIndex, this.fullTextIndex);
}
/**
@@ -162,7 +187,8 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
*/
@Override
public boolean isSpecialParameter() {
return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter();
return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter()
|| TextCriteria.class.isAssignableFrom(getType());
}
private boolean isNearParameter() {
@@ -181,5 +207,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
private boolean hasNearAnnotation() {
return parameter.getParameterAnnotation(Near.class) != null;
}
}
}

View File

@@ -17,12 +17,17 @@ package org.springframework.data.mongodb.repository.query;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.query.Term;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Mongo-specific {@link ParametersParameterAccessor} to allow access to the {@link Distance} parameter.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class MongoParametersParameterAccessor extends ParametersParameterAccessor implements MongoParameterAccessor {
@@ -77,4 +82,35 @@ public class MongoParametersParameterAccessor extends ParametersParameterAccesso
return (Point) value;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getFullText()
*/
@Override
public TextCriteria getFullText() {
int index = method.getParameters().getFullTextParameterIndex();
return index >= 0 ? potentiallyConvertFullText(getValue(index)) : null;
}
protected TextCriteria potentiallyConvertFullText(Object fullText) {
Assert.notNull(fullText, "Fulltext parameter must not be 'null'.");
if (fullText instanceof String) {
return TextCriteria.forDefaultLanguage().matching((String) fullText);
}
if (fullText instanceof Term) {
return TextCriteria.forDefaultLanguage().matching((Term) fullText);
}
if (fullText instanceof TextCriteria) {
return ((TextCriteria) fullText);
}
throw new IllegalArgumentException(String.format(
"Expected full text parameter to be one of String, Term or TextCriteria but found %s.",
ClassUtils.getShortName(fullText.getClass())));
}
}

View File

@@ -146,11 +146,7 @@ class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
@Override
protected Query complete(Criteria criteria, Sort sort) {
if (criteria == null) {
return null;
}
Query query = new Query(criteria).with(sort);
Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort);
if (LOG.isDebugEnabled()) {
LOG.debug("Created query " + query);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-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.
@@ -39,6 +39,7 @@ import org.springframework.util.StringUtils;
* Mongo specific implementation of {@link QueryMethod}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class MongoQueryMethod extends QueryMethod {
@@ -143,7 +144,7 @@ public class MongoQueryMethod extends QueryMethod {
}
/**
* Returns whether te query is a geo near query.
* Returns whether the query is a geo near query.
*
* @return
*/

View File

@@ -21,6 +21,7 @@ import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
@@ -77,6 +78,11 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
query.limit(tree.getMaxResults());
}
TextCriteria textCriteria = accessor.getFullText();
if (textCriteria != null) {
query.addCriteria(textCriteria);
}
String fieldSpec = this.getQueryMethod().getFieldSpecification();
if (!StringUtils.hasText(fieldSpec)) {
@@ -87,6 +93,7 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
BasicQuery result = new BasicQuery(query.getQueryObject().toString(), fieldSpec);
result.setSortObject(query.getSortObject());
return result;
} catch (JSONParseException o_O) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2012 the original author or authors.
* Copyright 2010-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.
@@ -40,6 +40,7 @@ import org.springframework.util.Assert;
* Repository base implementation for Mongo.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class SimpleMongoRepository<T, ID extends Serializable> implements MongoRepository<T, ID> {
@@ -232,4 +233,5 @@ public class SimpleMongoRepository<T, ID extends Serializable> implements MongoR
protected MongoEntityInformation<T, ID> getEntityInformation() {
return entityInformation;
}
}

View File

@@ -45,6 +45,7 @@ import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
@@ -600,6 +601,63 @@ public class QueryMapperUnitTests {
assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("foo", -1).get()));
}
/**
* @see DATAMONGO-973
*/
@Test
public void getMappedFieldsAppendsTextScoreFieldProperlyCorrectlyWhenNotPresent() {
Query query = new Query();
DBObject dbo = mapper.getMappedFields(query.getFieldsObject(),
context.getPersistentEntity(WithTextScoreProperty.class));
assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("score", new BasicDBObject("$meta", "textScore")).get()));
}
/**
* @see DATAMONGO-973
*/
@Test
public void getMappedFieldsReplacesTextScoreFieldProperlyCorrectlyWhenPresent() {
Query query = new Query();
query.fields().include("textScore");
DBObject dbo = mapper.getMappedFields(query.getFieldsObject(),
context.getPersistentEntity(WithTextScoreProperty.class));
assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("score", new BasicDBObject("$meta", "textScore")).get()));
}
/**
* @see DATAMONGO-973
*/
@Test
public void getMappedSortAppendsTextScoreProperlyWhenSortedByScore() {
Query query = new Query().with(new Sort("textScore"));
DBObject dbo = mapper
.getMappedSort(query.getSortObject(), context.getPersistentEntity(WithTextScoreProperty.class));
assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("score", new BasicDBObject("$meta", "textScore")).get()));
}
/**
* @see DATAMONGO-973
*/
@Test
public void getMappedSortIgnoresTextScoreWhenNotSortedByScore() {
Query query = new Query().with(new Sort("id"));
DBObject dbo = mapper
.getMappedSort(query.getSortObject(), context.getPersistentEntity(WithTextScoreProperty.class));
assertThat(dbo, equalTo(new BasicDBObjectBuilder().add("_id", 1).get()));
}
@Document
public class Foo {
@Id private ObjectId id;
@@ -675,4 +733,10 @@ public class QueryMapperUnitTests {
@DBRef Map<String, Sample> mapWithDBRef;
}
class WithTextScoreProperty {
@Id String id;
@TextScore @Field("score") Float textScore;
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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 org.springframework.data.mongodb.core.convert;
import static org.hamcrest.core.IsNull.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.springframework.data.mongodb.core.convert.MongoConverters.TermToStringConverter;
import org.springframework.data.mongodb.core.query.Term;
import org.springframework.data.mongodb.core.query.Term.Type;
/**
* @author Christoph Strobl
*/
public class TermToStringConverterUnitTests {
/**
* @DATAMONGO-973
*/
@Test
public void shouldNotConvertNull() {
assertThat(TermToStringConverter.INSTANCE.convert(null), nullValue());
}
/**
* @DATAMONGO-973
*/
@Test
public void shouldUseFormattedRepresentationForConversion() {
Term term = spy(new Term("foo", Type.WORD));
TermToStringConverter.INSTANCE.convert(term);
verify(term, times(1)).getFormatted();
}
}

View File

@@ -214,4 +214,14 @@ public class MongoPersistentEntityTestDummy<T> implements MongoPersistentEntity<
public String getLanguage() {
return null;
}
@Override
public MongoPersistentProperty getTextScoreProperty() {
return null;
}
@Override
public boolean hasTextScoreProperty() {
return false;
}
}

View File

@@ -91,6 +91,12 @@ public class IsQuery<T extends Query> extends TypeSafeMatcher<T> {
return this;
}
public IsQuery<T> where(Criteria criteria) {
this.query.putAll(criteria.getCriteriaObject());
return this;
}
@Override
public void describeTo(Description description) {
@@ -117,8 +123,10 @@ public class IsQuery<T extends Query> extends TypeSafeMatcher<T> {
return false;
}
if (!new IsEqual<DBObject>(sort).matches(item.getSortObject())) {
return false;
if (item.getSortObject() == null && !sort.toMap().isEmpty()) {
if (!new IsEqual<DBObject>(sort).matches(item.getSortObject())) {
return false;
}
}
if (!new IsEqual<DBObject>(fields).matches(item.getFieldsObject())) {

View File

@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
import org.hamcrest.TypeSafeMatcher;
import org.springframework.data.mongodb.core.query.IsQuery;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.TextQuery;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBObject;
@@ -28,7 +30,7 @@ import com.mongodb.DBObject;
* @author Christoph Strobl
* @param <T>
*/
public class IsTextQuery<T extends TextQuery> extends IsQuery<T> {
public class IsTextQuery<T extends Query> extends IsQuery<T> {
private final String SCORE_DEFAULT_FIELDNAME = "score";
private final DBObject META_TEXT_SCORE = new BasicDBObject("$meta", "textScore");
@@ -39,7 +41,7 @@ public class IsTextQuery<T extends TextQuery> extends IsQuery<T> {
super();
}
public static <T extends TextQuery> IsTextQuery<T> isTextQuery() {
public static <T extends Query> IsTextQuery<T> isTextQuery() {
return new IsTextQuery<T>();
}
@@ -77,6 +79,41 @@ public class IsTextQuery<T extends TextQuery> extends IsQuery<T> {
return this;
}
@Override
public IsTextQuery<T> where(Criteria criteria) {
super.where(criteria);
return this;
}
@Override
public IsTextQuery<T> excludingField(String fieldname) {
super.excludingField(fieldname);
return this;
}
@Override
public IsTextQuery<T> includingField(String fieldname) {
super.includingField(fieldname);
return this;
}
@Override
public IsTextQuery<T> limitingTo(int limit) {
super.limitingTo(limit);
return this;
}
@Override
public IsQuery<T> skippig(int skip) {
super.skippig(skip);
return this;
}
private void appendLanguage(String language) {
DBObject dbo = getOrCreateTextDbo();

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
import org.hamcrest.core.IsEqual;
import org.junit.Assert;
@@ -25,6 +25,8 @@ import com.mongodb.DBObject;
import com.mongodb.util.JSON;
/**
* Unit tests for {@link TextCriteria}.
*
* @author Christoph Strobl
*/
public class TextCriteriaUnitTests {

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.collection.IsEmptyCollection.*;
@@ -40,7 +40,9 @@ import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.mongodb.core.query.text.TextQueryTests.FullTextDoc.FullTextDocBuilder;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.core.query.TextQuery;
import org.springframework.data.mongodb.core.query.TextQueryTests.FullTextDoc.FullTextDocBuilder;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;

View File

@@ -13,17 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.query.text;
package org.springframework.data.mongodb.core.query;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.query.text.IsTextQuery.*;
import static org.springframework.data.mongodb.core.query.IsTextQuery.*;
import org.junit.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.query.Query;
/**
* Unit tests for {@link TextQuery}.
*
* @author Christoph Strobl
*/
public class TextQueryUnitTests {

View File

@@ -0,0 +1,319 @@
/*
* 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 org.springframework.data.mongodb.repository;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.core.IsEqual.*;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder;
import org.springframework.data.mongodb.core.index.TextIndexed;
import org.springframework.data.mongodb.core.mapping.TextScore;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
/**
* Integration tests for text searches on repository.
*
* @author Christoph Strobl
* @author Oliver Gierke
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MongoRepositoryTextSearchIntegrationTests {
public static @ClassRule MongoVersionRule versionRule = MongoVersionRule.atLeast(new Version(2, 6, 0));
private static final FullTextDocument PASSENGER_57 = new FullTextDocument("1", "Passenger 57",
"Passenger 57 is an action film that stars Wesley Snipes and Bruce Payne.");
private static final FullTextDocument DEMOLITION_MAN = new FullTextDocument("2", "Demolition Man",
"Demolition Man is a science fiction action comedy film staring Wesley Snipes and Sylvester Stallone.");
private static final FullTextDocument DROP_ZONE = new FullTextDocument("3", "Drop Zone",
"Drop Zone is an action film featuring Wesley Snipes and Gary Busey.");
@Autowired MongoTemplate template;
FullTextRepository repo;
@Before
public void setUp() {
template.indexOps(FullTextDocument.class).ensureIndex(
new TextIndexDefinitionBuilder().onField("title").onField("content").build());
this.repo = new MongoRepositoryFactory(this.template).getRepository(FullTextRepository.class);
}
@After
public void tearDown() {
template.dropCollection(FullTextDocument.class);
}
/**
* @see DATAMONGO-973
*/
@Test
public void findAllByTextCriteriaShouldReturnMatchingDocuments() {
initRepoWithDefaultDocuments();
List<FullTextDocument> result = repo.findAllBy(TextCriteria.forDefaultLanguage().matchingAny("stallone", "payne"));
assertThat(result, hasSize(2));
assertThat(result, hasItems(PASSENGER_57, DEMOLITION_MAN));
}
/**
* @see DATAMONGO-973
*/
@Test
public void derivedFinderWithTextCriteriaReturnsCorrectResult() {
initRepoWithDefaultDocuments();
FullTextDocument blade = new FullTextDocument(
"4",
"Blade",
"Blade is a 1998 American vampire-superhero-vigilante action film starring Wesley Snipes and Stephen Dorff, loosely based on the Marvel Comics character Blade");
blade.nonTextIndexProperty = "foo";
repo.save(blade);
List<FullTextDocument> result = repo.findByNonTextIndexProperty("foo",
TextCriteria.forDefaultLanguage().matching("snipes"));
assertThat(result, hasSize(1));
assertThat(result, hasItems(blade));
}
/**
* @see DATAMONGO-973
*/
@Test
public void findByWithPaginationWorksCorrectlyWhenUsingTextCriteria() {
initRepoWithDefaultDocuments();
Page<FullTextDocument> page = repo.findAllBy(TextCriteria.forDefaultLanguage().matching("film"), new PageRequest(1,
1, Direction.ASC, "id"));
assertThat(page.hasNext(), is(true));
assertThat(page.hasPrevious(), is(true));
assertThat(page.getTotalElements(), is(3L));
assertThat(page.getContent().get(0), equalTo(DEMOLITION_MAN));
}
/**
* @see DATAMONGO-973
*/
@Test
public void findAllByTextCriteriaWithSortWorksCorrectly() {
initRepoWithDefaultDocuments();
FullTextDocument snipes = new FullTextDocument("4", "Snipes", "Wesley Trent Snipes is an actor and film producer.");
repo.save(snipes);
List<FullTextDocument> result = repo.findAllBy(TextCriteria.forDefaultLanguage().matching("snipes"), new Sort(
"score"));
assertThat(result.size(), is(4));
assertThat(result.get(0), equalTo(snipes));
}
/**
* @see DATAMONGO-973
*/
@Test
public void findByWithSortByScoreViaPageRequestTriggersSortingCorrectly() {
initRepoWithDefaultDocuments();
FullTextDocument snipes = new FullTextDocument("4", "Snipes", "Wesley Trent Snipes is an actor and film producer.");
repo.save(snipes);
Page<FullTextDocument> page = repo.findAllBy(TextCriteria.forDefaultLanguage().matching("snipes"), new PageRequest(
0, 10, Direction.ASC, "score"));
assertThat(page.getTotalElements(), is(4L));
assertThat(page.getContent().get(0), equalTo(snipes));
}
/**
* @see DATAMONGO-973
*/
@Test
public void findByWithSortViaPageRequestIgnoresTextScoreWhenSortedByOtherProperty() {
initRepoWithDefaultDocuments();
FullTextDocument snipes = new FullTextDocument("4", "Snipes", "Wesley Trent Snipes is an actor and film producer.");
repo.save(snipes);
Page<FullTextDocument> page = repo.findAllBy(TextCriteria.forDefaultLanguage().matching("snipes"), new PageRequest(
0, 10, Direction.ASC, "id"));
assertThat(page.getTotalElements(), is(4L));
assertThat(page.getContent().get(0), equalTo(PASSENGER_57));
}
/**
* @see DATAMONGO-973
*/
@Test
public void derivedSortForTextScorePropertyWorksCorrectly() {
initRepoWithDefaultDocuments();
FullTextDocument snipes = new FullTextDocument("4", "Snipes", "Wesley Trent Snipes is an actor and film producer.");
repo.save(snipes);
List<FullTextDocument> result = repo.findByNonTextIndexPropertyIsNullOrderByScoreDesc(TextCriteria
.forDefaultLanguage().matching("snipes"));
assertThat(result.get(0), equalTo(snipes));
}
/**
* @see DATAMONGO-973
*/
@Test
public void derivedFinderMethodWithoutFullTextShouldNoCauseTroubleWhenHavingEntityWithTextScoreProperty() {
initRepoWithDefaultDocuments();
List<FullTextDocument> result = repo.findByTitle(DROP_ZONE.getTitle());
assertThat(result.get(0), equalTo(DROP_ZONE));
assertThat(result.get(0).score, equalTo(0.0F));
}
private void initRepoWithDefaultDocuments() {
repo.save(Arrays.asList(PASSENGER_57, DEMOLITION_MAN, DROP_ZONE));
}
@org.springframework.context.annotation.Configuration
public static class Configuration extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return ClassUtils.getShortNameAsProperty(MongoRepositoryTextSearchIntegrationTests.class);
}
@Override
public Mongo mongo() throws Exception {
return new MongoClient();
}
}
static class FullTextDocument {
private @Id String id;
private @TextIndexed String title;
private @TextIndexed String content;
String nonTextIndexProperty;
@TextScore Float score;
public FullTextDocument() {
}
public FullTextDocument(String id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
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;
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.id);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof FullTextDocument)) {
return false;
}
FullTextDocument other = (FullTextDocument) obj;
return ObjectUtils.nullSafeEquals(this.id, other.id);
}
}
static interface FullTextRepository extends MongoRepository<FullTextDocument, String> {
List<FullTextDocument> findByNonTextIndexProperty(String nonTextIndexProperty, TextCriteria criteria);
List<FullTextDocument> findByNonTextIndexPropertyIsNullOrderByScoreDesc(TextCriteria criteria);
List<FullTextDocument> findByTitle(String title);
List<FullTextDocument> findAllBy(TextCriteria textCriteria);
List<FullTextDocument> findAllBy(TextCriteria textCriteria, Sort sort);
Page<FullTextDocument> findAllBy(TextCriteria textCriteria, Pageable pageable);
}
}

View File

@@ -21,11 +21,13 @@ import static org.junit.Assert.*;
import java.lang.reflect.Method;
import java.util.List;
import org.hamcrest.core.IsNull;
import org.junit.Test;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -35,6 +37,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
* Unit tests for {@link MongoParametersParameterAccessor}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class MongoParametersParameterAccessorUnitTests {
@@ -64,10 +67,41 @@ public class MongoParametersParameterAccessorUnitTests {
assertThat(accessor.getMaxDistance(), is(DISTANCE));
}
/**
* @see DATAMONGO-973
*/
@Test
public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException {
Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, context);
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] {
new Point(10, 20), DISTANCE });
assertThat(accessor.getFullText(), IsNull.nullValue());
}
/**
* @see DATAMONGO-973
*/
@Test
public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException {
Method method = PersonRepository.class.getMethod("findByFirstname", String.class, TextCriteria.class);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, context);
MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { "spring",
TextCriteria.forDefaultLanguage().matching("data") });
assertThat(accessor.getFullText().getCriteriaObject().toString(),
equalTo("{ \"$text\" : { \"$search\" : \"data\"}}"));
}
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLocationNear(Point point);
List<Person> findByLocationNear(Point point, Distance distance);
List<Person> findByFirstname(String firstname, TextCriteria fullText);
}
}

View File

@@ -28,6 +28,7 @@ import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.Near;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.repository.query.Parameter;
@@ -36,6 +37,7 @@ import org.springframework.data.repository.query.Parameter;
* Unit tests for {@link MongoParameters}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoParametersUnitTests {
@@ -97,6 +99,28 @@ public class MongoParametersUnitTests {
assertThat(parameters.getNearIndex(), is(1));
}
/**
* @see DATAMONGO-973
*/
@Test
public void shouldFindTextCriteriaAtItsIndex() throws SecurityException, NoSuchMethodException {
Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class);
MongoParameters parameters = new MongoParameters(method, false);
assertThat(parameters.getFullTextParameterIndex(), is(1));
}
/**
* @see DATAMONGO-973
*/
@Test
public void shouldTreatTextCriteriaParameterAsSpecialParameter() throws SecurityException, NoSuchMethodException {
Method method = PersonRepository.class.getMethod("findByNameAndText", String.class, TextCriteria.class);
MongoParameters parameters = new MongoParameters(method, false);
assertThat(parameters.getParameter(parameters.getFullTextParameterIndex()).isSpecialParameter(), is(true));
}
interface PersonRepository {
List<Person> findByLocationNear(Point point, Distance distance);
@@ -110,5 +134,7 @@ public class MongoParametersUnitTests {
GeoResults<Person> findByOtherLocationAndLocationNear(Point point, @Near Point anotherLocation);
GeoResults<Person> validDoubleArrays(double[] first, @Near double[] second);
List<Person> findByNameAndText(String name, TextCriteria text);
}
}

View File

@@ -42,6 +42,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
* Unit test for {@link MongoQueryMethod}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class MongoQueryMethodUnitTests {

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.query;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.query.IsTextQuery.*;
import java.lang.reflect.Method;
@@ -36,6 +37,8 @@ import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.Query;
@@ -120,6 +123,18 @@ public class PartTreeMongoQueryUnitTests {
assertThat(query.getFieldsObject(), is(new BasicDBObjectBuilder().add("firstname", 0).add("lastname", 0).get()));
}
/**
* @see DATAMOGO-973
*/
@Test
public void shouldAddFullTextParamCorrectlyToDerivedQuery() {
org.springframework.data.mongodb.core.query.Query query = deriveQueryFromMethod("findPersonByFirstname",
new Object[] { "text", TextCriteria.forDefaultLanguage().matching("search") });
assertThat(query, isTextQuery().searchingFor("search").where(new Criteria("firstname").is("text")));
}
private org.springframework.data.mongodb.core.query.Query deriveQueryFromMethod(String method, Object[] args) {
Class<?>[] types = new Class<?>[args.length];
@@ -162,5 +177,7 @@ public class PartTreeMongoQueryUnitTests {
@Query(fields = "{ 'firstname' : 0, 'lastname' : 0 }")
Person findPersonByFirstnameAndLastname(String firstname, String lastname);
Person findPersonByFirstname(String firstname, TextCriteria fullText);
}
}

View File

@@ -23,6 +23,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.ParameterAccessor;
/**
@@ -105,4 +106,13 @@ class StubParameterAccessor implements MongoParameterAccessor {
public Point getGeoNearLocation() {
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getFullText()
*/
@Override
public TextCriteria getFullText() {
return null;
}
}

View File

@@ -296,6 +296,45 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
We think you will find this an extremely powerful tool for writing MongoDB queries.
[[mongodb.repositories.queries.full-text]]
=== Full-text search queries
MongoDBs full text search feature is very store specic and therefore can rather be found on `MongoRepository` than on the more general `CrudRepository`. What we need is a document with a full-text index defined for (Please see section <<mapping-usage-indexes.text-index>> for creating).
Additional methods on `MongoRepository` take `TextCriteria` as input parameter. In addition to those explicit methods, it is also possible to add a `TextCriteria` derived repository method. The criteria will added as an additional `AND` criteria. Once the entity contains a `@TextScore` annotated property the documents full-text score will be retrieved. Furthermore the `@TextScore` annotated property will also make it possible to sort by the documents score.
[source, java]
----
@Document
class FullTextDocument {
@Id String id;
@TextIndexed String title;
@TextIndexed String content;
@TextScore Float score;
}
interface FullTextRepository extends Repository<FullTextDocument, String> {
// Execute a full-text search and define sorting dynamically
List<FullTextDocument> findAllBy(TextCriteria criteria, Sort sort);
// Paginate over a full-text search result
Page<FullTextDocument> findAllBy(TextCriteria criteria, Pageable pageable);
// Combine a derived query with a full-text search
List<FullTextDocument> findByTitleOrderByScoreDesc(String title, TextCriteria criteria);
}
Sort sort = new Sort("score");
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("spring", "data");
List<FullTextDocument> result = repository.findAllBy(criteria, sort);
criteria = TextCriteria.forDefaultLanguage().matching("film");
Page<FullTextDocument> page = repository.findAllBy(criteria, new PageRequest(1, 1, sort));
List<FullTextDocument> result = repository.findByTitleOrderByScoreDesc("mongodb", criteria);
----
[[mongodb.repositories.misc]]
== Miscellaneous