From 84913cecab66e710389a7a0f14c0eca5fd3a5cc0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 24 Jun 2014 11:31:24 +0200 Subject: [PATCH] DATAMONGO-937 - Add support for creating text index. We now support creating text index based on information gathered on domain types. Using @TextIndexed marks properties to be considered for the full text index. Use the weight attribute to influence document scoring during search operations. Please note that using @TextIndexed on entity properties forces all properties of any sub document to be considered as part of the text index. Any set weight will in that case be propagated to the siblings taking the most recent weight information into account, which means that a the weight attribute can be overridden for properties in sub documents. The setting the index default language can be done via @Document(language) while @Language can be used to define the language_override field. As text search is disabled by default for mongodb 2.4 we use a jUnit ClassRule to restrict integration tests potentially creating text index (as the entities for testing are found in the classpath) to only be executed in when a 2.6+ mongodb server is present. For usage hints please see section 6.3.4 (Text Indexes) of reference manual. Original pull request: #198. --- .../mongodb/core/DefaultIndexOperations.java | 14 +- .../data/mongodb/core/index/IndexField.java | 58 ++- .../data/mongodb/core/index/IndexInfo.java | 38 +- .../MongoPersistentEntityIndexResolver.java | 118 ++++++ .../core/index/TextIndexDefinition.java | 336 ++++++++++++++++++ .../data/mongodb/core/index/TextIndexed.java | 44 +++ .../mapping/BasicMongoPersistentEntity.java | 16 +- .../mapping/BasicMongoPersistentProperty.java | 11 + .../data/mongodb/core/mapping/Document.java | 12 +- .../data/mongodb/core/mapping/Language.java | 35 ++ .../core/mapping/MongoPersistentEntity.java | 12 +- .../core/mapping/MongoPersistentProperty.java | 12 +- ...entEntityIndexCreatorIntegrationTests.java | 16 +- ...ersistentEntityIndexResolverUnitTests.java | 174 ++++++++- .../mongodb/core/index/TextIndexTests.java | 96 +++++ .../BasicMongoPersistentEntityUnitTests.java | 22 +- ...BasicMongoPersistentPropertyUnitTests.java | 49 ++- .../MongoPersistentEntityTestDummy.java | 5 + .../ValidatingMongoEventListenerTests.java | 11 +- .../mongodb/test/util/MongoVersionRule.java | 109 ++++++ src/docbkx/reference/mapping.xml | 57 ++- src/docbkx/reference/mongodb.xml | 11 +- 22 files changed, 1210 insertions(+), 46 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexed.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Language.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/TextIndexTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index a09e25418..434862aaa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -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. @@ -36,6 +36,7 @@ import com.mongodb.MongoException; * @author Mark Pollack * @author Oliver Gierke * @author Komi Innocent + * @author Christoph Strobl */ public class DefaultIndexOperations implements IndexOperations { @@ -142,6 +143,13 @@ public class DefaultIndexOperations implements IndexOperations { if ("2d".equals(value)) { indexFields.add(IndexField.geo(key)); + } else if ("text".equals(value)) { + + DBObject weights = (DBObject) ix.get("weights"); + for (String fieldName : weights.keySet()) { + indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); + } + } else { Double keyValue = new Double(value.toString()); @@ -159,8 +167,8 @@ public class DefaultIndexOperations implements IndexOperations { boolean unique = ix.containsField("unique") ? (Boolean) ix.get("unique") : false; boolean dropDuplicates = ix.containsField("dropDups") ? (Boolean) ix.get("dropDups") : false; boolean sparse = ix.containsField("sparse") ? (Boolean) ix.get("sparse") : false; - - indexInfoList.add(new IndexInfo(indexFields, name, unique, dropDuplicates, sparse)); + String language = ix.containsField("default_language") ? (String) ix.get("default_language") : ""; + indexInfoList.add(new IndexInfo(indexFields, name, unique, dropDuplicates, sparse, language)); } return indexInfoList; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java index 79cddfff3..83bf35436 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -24,22 +24,33 @@ import org.springframework.util.ObjectUtils; * Value object for an index field. * * @author Oliver Gierke + * @author Christoph Strobl */ @SuppressWarnings("deprecation") public final class IndexField { + enum Type { + GEO, TEXT, DEFAULT; + } + private final String key; private final Direction direction; - private final boolean isGeo; + private final Type type; + private final Float weight; - private IndexField(String key, Direction direction, boolean isGeo) { + private IndexField(String key, Direction direction, Type type) { + this(key, direction, type, Float.NaN); + } + + private IndexField(String key, Direction direction, Type type, Float weight) { Assert.hasText(key); - Assert.isTrue(direction != null ^ isGeo); + Assert.isTrue(direction != null ^ (Type.GEO.equals(type) || Type.TEXT.equals(type))); this.key = key; this.direction = direction; - this.isGeo = isGeo; + this.type = type == null ? Type.DEFAULT : type; + this.weight = weight == null ? Float.NaN : weight; } /** @@ -53,12 +64,12 @@ public final class IndexField { @Deprecated public static IndexField create(String key, Order order) { Assert.notNull(order); - return new IndexField(key, order.toDirection(), false); + return new IndexField(key, order.toDirection(), Type.DEFAULT); } public static IndexField create(String key, Direction order) { Assert.notNull(order); - return new IndexField(key, order, false); + return new IndexField(key, order, Type.DEFAULT); } /** @@ -68,7 +79,16 @@ public final class IndexField { * @return */ public static IndexField geo(String key) { - return new IndexField(key, null, true); + return new IndexField(key, null, Type.GEO); + } + + /** + * Creates a text {@link IndexField} for the given key. + * + * @since 1.6 + */ + public static IndexField text(String key, Float weight) { + return new IndexField(key, null, Type.TEXT, weight); } /** @@ -101,10 +121,20 @@ public final class IndexField { /** * Returns whether the {@link IndexField} is a geo index field. * - * @return the isGeo + * @return true if type is {@link Type#GEO}. */ public boolean isGeo() { - return isGeo; + return Type.GEO.equals(type); + } + + /** + * Returns wheter the {@link IndexField} is a text index field. + * + * @return true if type is {@link Type#TEXT} + * @since 1.6 + */ + public boolean isText() { + return Type.TEXT.equals(type); } /* @@ -125,7 +155,7 @@ public final class IndexField { IndexField that = (IndexField) obj; return this.key.equals(that.key) && ObjectUtils.nullSafeEquals(this.direction, that.direction) - && this.isGeo == that.isGeo; + && this.type == that.type; } /* @@ -138,7 +168,8 @@ public final class IndexField { int result = 17; result += 31 * ObjectUtils.nullSafeHashCode(key); result += 31 * ObjectUtils.nullSafeHashCode(direction); - result += 31 * ObjectUtils.nullSafeHashCode(isGeo); + result += 31 * ObjectUtils.nullSafeHashCode(type); + result += 31 * ObjectUtils.nullSafeHashCode(weight); return result; } @@ -148,6 +179,7 @@ public final class IndexField { */ @Override public String toString() { - return String.format("IndexField [ key: %s, direction: %s, isGeo: %s]", key, direction, isGeo); + return String.format("IndexField [ key: %s, direction: %s, type: %s, weight: %s]", key, direction, type, weight); } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index 332a25d10..2073fc1c2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-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. @@ -23,6 +23,11 @@ import java.util.List; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +/** + * @author Mark Pollack + * @author Oliver Gierke + * @author Christoph Strobl + */ public class IndexInfo { private final List indexFields; @@ -31,14 +36,30 @@ public class IndexInfo { private final boolean unique; private final boolean dropDuplicates; private final boolean sparse; + private final String language; + /** + * @deprecated Will be removed in 1.7. Please use {@link #IndexInfo(List, String, boolean, boolean, boolean, String)} + * @param indexFields + * @param name + * @param unique + * @param dropDuplicates + * @param sparse + */ + @Deprecated public IndexInfo(List indexFields, String name, boolean unique, boolean dropDuplicates, boolean sparse) { + this(indexFields, name, unique, dropDuplicates, sparse, ""); + } + + public IndexInfo(List indexFields, String name, boolean unique, boolean dropDuplicates, boolean sparse, + String language) { this.indexFields = Collections.unmodifiableList(indexFields); this.name = name; this.unique = unique; this.dropDuplicates = dropDuplicates; this.sparse = sparse; + this.language = language; } /** @@ -84,14 +105,23 @@ public class IndexInfo { return sparse; } + /** + * @return + * @since 1.6 + */ + public String getLanguage() { + return language; + } + @Override public String toString() { return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", dropDuplicates=" - + dropDuplicates + ", sparse=" + sparse + "]"; + + dropDuplicates + ", sparse=" + sparse + ", language=" + language + "]"; } @Override public int hashCode() { + final int prime = 31; int result = 1; result = prime * result + (dropDuplicates ? 1231 : 1237); @@ -99,6 +129,7 @@ public class IndexInfo { result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + (sparse ? 1231 : 1237); result = prime * result + (unique ? 1231 : 1237); + result = prime * result + ObjectUtils.nullSafeHashCode(language); return result; } @@ -137,6 +168,9 @@ public class IndexInfo { if (unique != other.unique) { return false; } + if (!ObjectUtils.nullSafeEquals(language, other.language)) { + return false; + } return true; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 9c299125c..d51089a35 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.index; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -29,6 +30,9 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mongodb.core.index.Index.Duplicates; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.TextIndexIncludeOptions.IncludeStrategy; +import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder; +import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexedFieldSpec; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -93,6 +97,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { final List indexInformation = new ArrayList(); indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", root.getCollection(), root.getType())); + indexInformation.addAll(potentiallyCreateTextIndexDefinition(root)); final CycleGuard guard = new CycleGuard(); @@ -188,6 +193,82 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { return createCompoundIndexDefinitions(dotPath, collection, type); } + private Collection potentiallyCreateTextIndexDefinition(MongoPersistentEntity root) { + + TextIndexDefinitionBuilder indexDefinitionBuilder = new TextIndexDefinitionBuilder().named(root.getType() + .getSimpleName() + "_TextIndex"); + + if (StringUtils.hasText(root.getLanguage())) { + indexDefinitionBuilder.withDefaultLanguage(root.getLanguage()); + } + + try { + appendTextIndexInformation("", indexDefinitionBuilder, root, + new TextIndexIncludeOptions(IncludeStrategy.DEFAULT), new CycleGuard()); + } catch (CyclicPropertyReferenceException e) { + LOGGER.warn(e.getMessage()); + } + + TextIndexDefinition indexDefinition = indexDefinitionBuilder.build(); + + if (!indexDefinition.hasFieldSpec()) { + return Collections.emptyList(); + } + + IndexDefinitionHolder holder = new IndexDefinitionHolder("", indexDefinition, root.getCollection()); + return Collections.singletonList(holder); + + } + + private void appendTextIndexInformation(final String dotPath, + final TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity entity, + final TextIndexIncludeOptions includeOptions, final CycleGuard guard) { + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) { + + guard.protect(persistentProperty, dotPath); + + if (persistentProperty.isLanguageProperty()) { + indexDefinitionBuilder.withLanguageOverride(persistentProperty.getFieldName()); + } + + TextIndexed indexed = persistentProperty.findAnnotation(TextIndexed.class); + + if (includeOptions.isForce() || indexed != null || persistentProperty.isEntity()) { + + String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + + persistentProperty.getFieldName(); + + Float weight = indexed != null ? indexed.weight() + : (includeOptions.getParentFieldSpec() != null ? includeOptions.getParentFieldSpec().getWeight() : 1.0F); + + if (persistentProperty.isEntity()) { + + TextIndexIncludeOptions optionsForNestedType = includeOptions; + if (!IncludeStrategy.FORCE.equals(includeOptions.getStrategy()) && indexed != null) { + optionsForNestedType = new TextIndexIncludeOptions(IncludeStrategy.FORCE, new TextIndexedFieldSpec( + propertyDotPath, weight)); + } + + try { + appendTextIndexInformation(propertyDotPath, indexDefinitionBuilder, + mappingContext.getPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard); + } catch (CyclicPropertyReferenceException e) { + LOGGER.warn(e.getMessage(), e); + } + } else if (includeOptions.isForce() || indexed != null) { + indexDefinitionBuilder.onField(propertyDotPath, weight); + } + } + + } + }); + + } + /** * Create {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} for {@link CompoundIndexes} of given type. * @@ -549,4 +630,41 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { return indexDefinition.getIndexOptions(); } } + + /** + * @author Christoph Strobl + * @since 1.6 + */ + static class TextIndexIncludeOptions { + + enum IncludeStrategy { + FORCE, DEFAULT; + } + + private final IncludeStrategy strategy; + + private final TextIndexedFieldSpec parentFieldSpec; + + public TextIndexIncludeOptions(IncludeStrategy strategy, TextIndexedFieldSpec parentFieldSpec) { + this.strategy = strategy; + this.parentFieldSpec = parentFieldSpec; + } + + public TextIndexIncludeOptions(IncludeStrategy strategy) { + this(strategy, null); + } + + public IncludeStrategy getStrategy() { + return strategy; + } + + public TextIndexedFieldSpec getParentFieldSpec() { + return parentFieldSpec; + } + + public boolean isForce() { + return IncludeStrategy.FORCE.equals(strategy); + } + + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java new file mode 100644 index 000000000..8768d5591 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java @@ -0,0 +1,336 @@ +/* + * 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.index; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * {@link IndexDefinition} to span multiple keys for text search. + * + * @author Christoph Strobl + * @since 1.6 + */ +public class TextIndexDefinition implements IndexDefinition { + + private String name; + private Set fieldSpecs; + private String defaultLanguage; + private String languageOverride; + + TextIndexDefinition() { + fieldSpecs = new LinkedHashSet(); + } + + /** + * Creates a {@link TextIndexDefinition} for all fields in the document. + * + * @return + */ + public static TextIndexDefinition forAllFields() { + return new TextIndexDefinitionBuilder().onAllFields().build(); + } + + /** + * Get {@link TextIndexDefinitionBuilder} to create {@link TextIndexDefinition}. + * + * @return + */ + public static TextIndexDefinitionBuilder builder() { + return new TextIndexDefinitionBuilder(); + } + + /** + * @param fieldSpec + */ + public void addFieldSpec(TextIndexedFieldSpec fieldSpec) { + this.fieldSpecs.add(fieldSpec); + } + + /** + * @param fieldSpecs + */ + public void addFieldSpecs(Collection fieldSpecs) { + this.fieldSpecs.addAll(fieldSpecs); + } + + /** + * Returns if the {@link TextIndexDefinition} has fields assigned. + * + * @return + */ + public boolean hasFieldSpec() { + return !fieldSpecs.isEmpty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexKeys() + */ + @Override + public DBObject getIndexKeys() { + + DBObject keys = new BasicDBObject(); + for (TextIndexedFieldSpec fieldSpec : fieldSpecs) { + keys.put(fieldSpec.fieldname, "text"); + } + + return keys; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.index.IndexDefinition#getIndexOptions() + */ + @Override + public DBObject getIndexOptions() { + + DBObject options = new BasicDBObject(); + if (StringUtils.hasText(name)) { + options.put("name", name); + } + if (StringUtils.hasText(defaultLanguage)) { + options.put("default_language", defaultLanguage); + } + + BasicDBObject weightsDbo = new BasicDBObject(); + for (TextIndexedFieldSpec fieldSpec : fieldSpecs) { + if (fieldSpec.isWeighted()) { + weightsDbo.put(fieldSpec.getFieldname(), fieldSpec.getWeight()); + } + } + + if (!weightsDbo.isEmpty()) { + options.put("weights", weightsDbo); + } + if (StringUtils.hasText(languageOverride)) { + options.put("language_override", languageOverride); + } + + return options; + } + + /** + * @author Christoph Strobl + * @since 1.6 + */ + public static class TextIndexedFieldSpec { + + private final String fieldname; + private final Float weight; + + /** + * Create new {@link TextIndexedFieldSpec} for given fieldname without any weight. + * + * @param fieldname + */ + public TextIndexedFieldSpec(String fieldname) { + this(fieldname, 1.0F); + } + + /** + * Create new {@link TextIndexedFieldSpec} for given fieldname and weight. + * + * @param fieldname + * @param weight + */ + public TextIndexedFieldSpec(String fieldname, Float weight) { + + Assert.hasText(fieldname, "Text index field cannot be blank."); + this.fieldname = fieldname; + this.weight = weight != null ? weight : 1.0F; + } + + /** + * Get the fieldname associated with the {@link TextIndexedFieldSpec}. + * + * @return + */ + public String getFieldname() { + return fieldname; + } + + /** + * Get the weight associated with the {@link TextIndexedFieldSpec}. + * + * @return + */ + public Float getWeight() { + return weight; + } + + /** + * @return true if {@link #weight} has a value that is a valid number. + */ + public boolean isWeighted() { + return this.weight != null && this.weight.compareTo(1.0F) != 0; + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(fieldname); + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TextIndexedFieldSpec)) { + return false; + } + + TextIndexedFieldSpec other = (TextIndexedFieldSpec) obj; + + return ObjectUtils.nullSafeEquals(this.fieldname, other.fieldname); + } + + } + + /** + * {@link TextIndexDefinitionBuilder} helps defining options for creating {@link TextIndexDefinition}. + * + * @author Christoph Strobl + * @since 1.6 + */ + public static class TextIndexDefinitionBuilder { + + private TextIndexDefinition instance; + private static final TextIndexedFieldSpec ALL_FIELDS = new TextIndexedFieldSpec("$**"); + + public TextIndexDefinitionBuilder() { + this.instance = new TextIndexDefinition(); + } + + /** + * Define the name to be used when creating the index in the store. + * + * @param name + * @return + */ + public TextIndexDefinitionBuilder named(String name) { + this.instance.name = name; + return this; + } + + /** + * Define the index to span all fields using wilcard.
+ * NOTE {@link TextIndexDefinition} cannot contain any other fields when defined with wildcard. + * + * @return + */ + public TextIndexDefinitionBuilder onAllFields() { + + if (!instance.fieldSpecs.isEmpty()) { + throw new InvalidDataAccessApiUsageException("Cannot add wildcard fieldspect to non empty."); + } + + this.instance.fieldSpecs.add(ALL_FIELDS); + return this; + } + + /** + * Include given fields with default weight. + * + * @param fieldnames + * @return + */ + public TextIndexDefinitionBuilder onFields(String... fieldnames) { + + for (String fieldname : fieldnames) { + onField(fieldname); + } + return this; + } + + /** + * Include given field with default weight. + * + * @param fieldname + * @return + */ + public TextIndexDefinitionBuilder onField(String fieldname) { + return onField(fieldname, Float.NaN); + } + + /** + * Include given field with weight. + * + * @param fieldname + * @return + */ + public TextIndexDefinitionBuilder onField(String fieldname, Float weight) { + + if (this.instance.fieldSpecs.contains(ALL_FIELDS)) { + throw new InvalidDataAccessApiUsageException(String.format("Cannot add %s to field spec for all fields.", + fieldname)); + } + + this.instance.fieldSpecs.add(new TextIndexedFieldSpec(fieldname, weight)); + return this; + } + + /** + * Define the default language to be used when indexing documents. + * + * @param language + * @see http://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/#specify-default-language-text-index + * @return + */ + public TextIndexDefinitionBuilder withDefaultLanguage(String language) { + + this.instance.defaultLanguage = language; + return this; + } + + /** + * Define field for language override. + * + * @param fieldname + * @return + */ + public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) { + + if (StringUtils.hasText(this.instance.languageOverride)) { + throw new InvalidDataAccessApiUsageException(String.format( + "Cannot set language override on %s as it is already defined on %s.", fieldname, + this.instance.languageOverride)); + } + + this.instance.languageOverride = fieldname; + return this; + } + + public TextIndexDefinition build() { + return this.instance; + } + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexed.java new file mode 100644 index 000000000..9348aacd4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexed.java @@ -0,0 +1,44 @@ +/* + * 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.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@link TextIndexed} marks a field to be part of the text index. As there can be only one text index per collection + * all fields marked with {@link TextIndexed} are combined into one single index.
+ * + * @author Christoph Strobl + * @since 1.6 + */ +@Documented +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface TextIndexed { + + /** + * Defines the significance of the filed relative to other indexed fields. The value directly influences the documents + * score.
+ * Defaulted to {@literal 1.0}. + * + * @return + */ + float weight() default 1.0F; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index dfa1e7465..2f9eac702 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -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. @@ -47,12 +47,14 @@ import org.springframework.util.StringUtils; * @author Jon Brisbin * @author Oliver Gierke * @author Thomas Darimont + * @author Christoph Strobl */ public class BasicMongoPersistentEntity extends BasicPersistentEntity implements MongoPersistentEntity, ApplicationContextAware { private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @DocumentField annotation!"; private final String collection; + private final String language; private final SpelExpressionParser parser; private final StandardEvaluationContext context; @@ -75,8 +77,10 @@ public class BasicMongoPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity implements MongoPersistentProperty { @@ -48,6 +49,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope private static final Logger LOG = LoggerFactory.getLogger(BasicMongoPersistentProperty.class); private static final String ID_FIELD_NAME = "_id"; + private static final String LANGUAGE_FIELD_NAME = "language"; private static final Set> SUPPORTED_ID_TYPES = new HashSet>(); private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); @@ -181,4 +183,13 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope public DBRef getDBRef() { return findAnnotation(DBRef.class); } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.mapping.MongoPersistentProperty#isLanguageProperty() + */ + @Override + public boolean isLanguageProperty() { + return getFieldName().equals(LANGUAGE_FIELD_NAME) || isAnnotationPresent(Language.class); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java index cb14e0f0d..3ff03c639 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Document.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 by the original author(s). + * Copyright (c) 2011-2014 by the original author(s). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.data.annotation.Persistent; * * @author Jon Brisbin * @author Oliver Gierke ogierke@vmware.com + * @author Christoph Strobl */ @Persistent @Inherited @@ -37,4 +38,13 @@ import org.springframework.data.annotation.Persistent; public @interface Document { String collection() default ""; + + /** + * Defines the default language to be used with this document. + * + * @since 1.6 + * @return + */ + String language() default ""; + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Language.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Language.java new file mode 100644 index 000000000..dc763264b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Language.java @@ -0,0 +1,35 @@ +/* + * 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.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark property as language field. + * + * @author Christoph Strobl + * @since 1.6 + */ +@Documented +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Language { + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java index bca73c585..b4085f8d9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2012 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. @@ -21,6 +21,7 @@ import org.springframework.data.mapping.PersistentEntity; * MongoDB specific {@link PersistentEntity} abstraction. * * @author Oliver Gierke + * @author Christoph Strobl */ public interface MongoPersistentEntity extends PersistentEntity { @@ -30,4 +31,13 @@ public interface MongoPersistentEntity extends PersistentEntity { @@ -59,6 +60,15 @@ public interface MongoPersistentProperty extends PersistentProperty indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnSinglePropertyInRoot.class); + assertThat(indexDefinitions.size(), equalTo(1)); + assertIndexPathAndCollection("bar", "textIndexOnSinglePropertyInRoot", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldResolveMultiFieldTextIndexCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnMutiplePropertiesInRoot.class); + assertThat(indexDefinitions.size(), equalTo(1)); + assertIndexPathAndCollection(new String[] { "foo", "bar" }, "textIndexOnMutiplePropertiesInRoot", + indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldResolveTextIndexOnElementCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnNestedRoot.class); + assertThat(indexDefinitions.size(), equalTo(1)); + assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedRoot", indexDefinitions.get(0)); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldResolveTextIndexOnElementWithWeightCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnNestedWithWeightRoot.class); + assertThat(indexDefinitions.size(), equalTo(1)); + assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedWithWeightRoot", + indexDefinitions.get(0)); + + DBObject weights = DBObjectTestUtils.getAsDBObject(indexDefinitions.get(0).getIndexOptions(), "weights"); + assertThat(weights.get("nested.foo"), IsEqual. equalTo(5F)); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldResolveTextIndexOnElementWithMostSpecificWeightCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(TextIndexOnNestedWithMostSpecificValueRoot.class); + assertThat(indexDefinitions.size(), equalTo(1)); + assertIndexPathAndCollection(new String[] { "nested.foo", "nested.bar" }, + "textIndexOnNestedWithMostSpecificValueRoot", indexDefinitions.get(0)); + + DBObject weights = DBObjectTestUtils.getAsDBObject(indexDefinitions.get(0).getIndexOptions(), "weights"); + assertThat(weights.get("nested.foo"), IsEqual. equalTo(5F)); + assertThat(weights.get("nested.bar"), IsEqual. equalTo(10F)); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldSetDefaultLanguageCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(DocumentWithDefaultLanguage.class); + assertThat(indexDefinitions.get(0).getIndexOptions().get("default_language"), IsEqual. equalTo("spanish")); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldResolveTextIndexLanguageOverrideCorrectly() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(DocumentWithLanguageOverrideOnNestedElementRoot.class); + assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), IsEqual. equalTo("lang")); + } + + @Document + static class TextIndexOnSinglePropertyInRoot { + + String foo; + + @TextIndexed String bar; + } + + @Document + static class TextIndexOnMutiplePropertiesInRoot { + + @TextIndexed String foo; + + @TextIndexed(weight = 5) String bar; + } + + @Document + static class TextIndexOnNestedRoot { + + String bar; + + @TextIndexed TextIndexOnNested nested; + } + + static class TextIndexOnNested { + + String foo; + + } + + @Document + static class TextIndexOnNestedWithWeightRoot { + + @TextIndexed(weight = 5) TextIndexOnNested nested; + + } + + @Document + static class TextIndexOnNestedWithMostSpecificValueRoot { + @TextIndexed(weight = 5) TextIndexOnNestedWithMostSpecificValue nested; + } + + static class TextIndexOnNestedWithMostSpecificValue { + + String foo; + @TextIndexed(weight = 10) String bar; + } + + @Document(language = "spanish") + static class DocumentWithDefaultLanguage { + @TextIndexed String foo; + } + + @Document + static class DocumentWithLanguageOverrideOnNestedElementRoot { + + DocumentWithLanguageOverrideOnNestedElement nested; + } + + static class DocumentWithLanguageOverrideOnNestedElement { + + @TextIndexed String foo; + + @Language String lang; + } + + } + public static class MixedIndexResolutionTests { /** @@ -561,16 +722,23 @@ public class MongoPersistentEntityIndexResolverUnitTests { public String collection() { return null; } + + @Override + public String language() { + return null; + } }; MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class); when(propertyMock.isEntity()).thenReturn(true); + when(propertyMock.getOwner()).thenReturn( + (PersistentEntity) MongoPersistentEntityDummyBuilder.forClass(Object.class).build()); when(propertyMock.getActualType()).thenThrow( new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar")); MongoPersistentEntity dummy = MongoPersistentEntityDummyBuilder - .forClass(SelfCyclingViaCollectionType.class).withCollection("foo").and(propertyMock) - .and(documentDummy).build(); + .forClass(SelfCyclingViaCollectionType.class).withCollection("foo").and(propertyMock).and(documentDummy) + .build(); new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class)) .resolveIndexForEntity(dummy); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/TextIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/TextIndexTests.java new file mode 100644 index 000000000..8047f0fe0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/TextIndexTests.java @@ -0,0 +1,96 @@ +/* + * 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.index; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.config.AbstractIntegrationTests; +import org.springframework.data.mongodb.core.IndexOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Language; +import org.springframework.data.mongodb.test.util.MongoVersionRule; +import org.springframework.data.util.Version; + +import com.mongodb.WriteConcern; + +/** + * @author Christoph Strobl + */ +public class TextIndexTests extends AbstractIntegrationTests { + + public static @ClassRule MongoVersionRule version = MongoVersionRule.atLeast(new Version(2, 6)); + + private @Autowired MongoTemplate template; + private IndexOperations indexOps; + + @Before + public void setUp() throws Exception { + + template.setWriteConcern(WriteConcern.FSYNC_SAFE); + this.indexOps = template.indexOps(TextIndexedDocumentRoot.class); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void indexInfoShouldHaveBeenCreatedCorrectly() { + + List indexInfos = indexOps.getIndexInfo(); + + assertThat(indexInfos.size(), is(2)); + + List fields = indexInfos.get(0).getIndexFields(); + assertThat(fields.size(), is(1)); + assertThat(fields, hasItem(IndexField.create("_id", Direction.ASC))); + + IndexInfo textIndexInfo = indexInfos.get(1); + List textIndexFields = textIndexInfo.getIndexFields(); + assertThat(textIndexFields.size(), is(4)); + assertThat(textIndexFields, hasItem(IndexField.text("textIndexedPropertyWithDefaultWeight", 1F))); + assertThat(textIndexFields, hasItem(IndexField.text("textIndexedPropertyWithWeight", 5F))); + assertThat(textIndexFields, hasItem(IndexField.text("nestedDocument.textIndexedPropertyInNestedDocument", 1F))); + assertThat(textIndexFields, hasItem(IndexField.create("_ftsx", Direction.ASC))); + assertThat(textIndexInfo.getLanguage(), is("spanish")); + } + + @Document(language = "spanish") + static class TextIndexedDocumentRoot { + + @TextIndexed String textIndexedPropertyWithDefaultWeight; + @TextIndexed(weight = 5) String textIndexedPropertyWithWeight; + + TextIndexedDocumentWihtLanguageOverride nestedDocument; + } + + static class TextIndexedDocumentWihtLanguageOverride { + + @Language String lang; + + @TextIndexed String textIndexedPropertyInNestedDocument; + + String nonTextIndexedProperty; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java index abf624c6e..ac2ff1f9e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 by the original author(s). + * Copyright 2011-2014 by the original author(s). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,12 +30,12 @@ import org.springframework.data.util.ClassTypeInformation; * Unit tests for {@link BasicMongoPersistentEntity}. * * @author Oliver Gierke + * @author Christoph Strobl */ @RunWith(MockitoJUnitRunner.class) public class BasicMongoPersistentEntityUnitTests { - @Mock - ApplicationContext context; + @Mock ApplicationContext context; @Test public void subclassInheritsAtDocumentAnnotation() { @@ -69,6 +69,17 @@ public class BasicMongoPersistentEntityUnitTests { assertThat(entity.getCollection(), is("reference")); } + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldDetectLanguageCorrectly() { + + BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity( + ClassTypeInformation.from(DocumentWithLanguage.class)); + assertThat(entity.getLanguage(), is("spanish")); + } + @Document(collection = "contacts") class Contact { @@ -95,4 +106,9 @@ public class BasicMongoPersistentEntityUnitTests { return collectionName; } } + + @Document(language = "spanish") + static class DocumentWithLanguage { + + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java index eaade63a4..d8fe99fbc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 by the original author(s). + * Copyright 2011-2014 by the original author(s). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import org.springframework.util.ReflectionUtils; * Unit test for {@link BasicMongoPersistentProperty}. * * @author Oliver Gierke + * @author Christoph Strobl */ public class BasicMongoPersistentPropertyUnitTests { @@ -123,8 +124,42 @@ public class BasicMongoPersistentPropertyUnitTests { property.getFieldName(); } + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldDetectAnnotatedLanguagePropertyCorrectly() { + + BasicMongoPersistentEntity persistentEntity = new BasicMongoPersistentEntity( + ClassTypeInformation.from(DocumentWithLanguageProperty.class)); + + MongoPersistentProperty property = getPropertyFor(persistentEntity, "lang"); + assertThat(property.isLanguageProperty(), is(true)); + } + + /** + * @see DATAMONGO-937 + */ + @Test + public void shouldDetectIplicitLanguagePropertyCorrectly() { + + BasicMongoPersistentEntity persistentEntity = new BasicMongoPersistentEntity( + ClassTypeInformation.from(DocumentWithImplicitLanguageProperty.class)); + + MongoPersistentProperty property = getPropertyFor(persistentEntity, "language"); + assertThat(property.isLanguageProperty(), is(true)); + } + private MongoPersistentProperty getPropertyFor(Field field) { - return new BasicMongoPersistentProperty(field, null, entity, new SimpleTypeHolder(), + return getPropertyFor(entity, field); + } + + private MongoPersistentProperty getPropertyFor(MongoPersistentEntity persistentEntity, String fieldname) { + return getPropertyFor(persistentEntity, ReflectionUtils.findField(persistentEntity.getType(), fieldname)); + } + + private MongoPersistentProperty getPropertyFor(MongoPersistentEntity persistentEntity, Field field) { + return new BasicMongoPersistentProperty(field, null, persistentEntity, new SimpleTypeHolder(), PropertyNameFieldNamingStrategy.INSTANCE); } @@ -155,4 +190,14 @@ public class BasicMongoPersistentPropertyUnitTests { return null; } } + + static class DocumentWithLanguageProperty { + + @Language String lang; + } + + static class DocumentWithImplicitLanguageProperty { + + String language; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntityTestDummy.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntityTestDummy.java index 8512a6edd..7577edd13 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntityTestDummy.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntityTestDummy.java @@ -209,4 +209,9 @@ public class MongoPersistentEntityTestDummy implements MongoPersistentEntity< } } + + @Override + public String getLanguage() { + return null; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests.java index 9eba54c61..8a8d06cd3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-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. @@ -20,10 +20,13 @@ import static org.junit.Assert.*; import javax.validation.ConstraintViolationException; +import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; +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; @@ -33,13 +36,15 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; * @see DATAMONGO-36 * @author Maciej Walkowiak * @author Oliver Gierke + * @author Christoph Strobl */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class ValidatingMongoEventListenerTests { - @Autowired - MongoTemplate mongoTemplate; + public static @ClassRule MongoVersionRule version = MongoVersionRule.atLeast(new Version(2, 6)); + + @Autowired MongoTemplate mongoTemplate; @Test public void shouldThrowConstraintViolationException() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java new file mode 100644 index 000000000..8d4feffa1 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoVersionRule.java @@ -0,0 +1,109 @@ +/* + * 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.test.util; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.internal.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.data.util.Version; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.CommandResult; +import com.mongodb.DB; +import com.mongodb.MongoClient; + +/** + * {@link TestRule} verifying server tests are executed against match a given version. This one can be used as + * {@link ClassRule} eg. in context depending tests run with {@link SpringJUnit4ClassRunner} when the context would fail + * to start in case of invalid version, or as simple {@link Rule} on specific tests. + * + * @author Christoph Strobl + * @since 1.6 + */ +public class MongoVersionRule implements TestRule { + + private String host = "localhost"; + private int port = 27017; + + private final Version minVersion; + private final Version maxVersion; + + private Version currentVersion; + + public MongoVersionRule(Version min, Version max) { + this.minVersion = min; + this.maxVersion = max; + } + + public static MongoVersionRule any() { + return new MongoVersionRule(new Version(0, 0, 0), new Version(9999, 9999, 9999)); + } + + public static MongoVersionRule atLeast(Version minVersion) { + return new MongoVersionRule(minVersion, new Version(9999, 9999, 9999)); + } + + public static MongoVersionRule atMost(Version maxVersion) { + return new MongoVersionRule(new Version(0, 0, 0), maxVersion); + } + + public MongoVersionRule withServerRunningAt(String host, int port) { + this.host = host; + this.port = port; + + return this; + } + + @Override + public Statement apply(final Statement base, Description description) { + + initCurrentVersion(); + return new Statement() { + + @Override + public void evaluate() throws Throwable { + if (currentVersion != null) { + if (currentVersion.isLessThan(minVersion) || currentVersion.isGreaterThan(maxVersion)) { + throw new AssumptionViolatedException(String.format( + "Expected mongodb server to be in range %s to %s but found %s", minVersion, maxVersion, currentVersion)); + } + } + base.evaluate(); + } + }; + } + + private void initCurrentVersion() { + + if (currentVersion == null) { + try { + MongoClient client; + client = new MongoClient(host, port); + DB db = client.getDB("test"); + CommandResult result = db.command(new BasicDBObjectBuilder().add("buildInfo", 1).get()); + this.currentVersion = Version.parse(result.get("version").toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + } + +} diff --git a/src/docbkx/reference/mapping.xml b/src/docbkx/reference/mapping.xml index 91675e4d6..41fb77e39 100644 --- a/src/docbkx/reference/mapping.xml +++ b/src/docbkx/reference/mapping.xml @@ -357,6 +357,16 @@ public class Person { level to describe how to geoindex the field. + + @TextIndexed - applied at the field level + to mark the field to be included in the text index. + + + + @Language - applied at the field level to + set the language override property for text index. + + @Transient - by default all private fields are mapped to the document, this annotation excludes the field where @@ -526,7 +536,7 @@ OrderItem item = converter.read(OrderItem.class, input); test suite. -
+
Compound Indexes Compound indexes are also supported. They are defined at the class @@ -561,6 +571,51 @@ public class Person {
+
+ Text Indexes + + + The text index feature is disabled by default for mongodb + v.2.4. + + + Creating a text index allows to accumulate several fields into a + searchable full text index. It is only possible to have one text index + per collection so all fields marked with + @TextIndexed are combined into this + index. Properties can be weighted to influence document score for + ranking results. The default language for the text index is english, to + change the default language set + @Document(language="spanish") to any + language you want. Using a property called language + or @Language allows to define a language + override on a per document base. + + + Example Text Index Usage + + @Document(language = "spanish") +class SomeEntity { + + @TextIndexed String foo; + + @Language String lang; + + Nested nested; + + +} + +class Nested { + + @TextIndexed(weight=5) String bar; + + String roo; +} + + +
+
Using DBRefs diff --git a/src/docbkx/reference/mongodb.xml b/src/docbkx/reference/mongodb.xml index 4f24eff44..d85b28ef0 100644 --- a/src/docbkx/reference/mongodb.xml +++ b/src/docbkx/reference/mongodb.xml @@ -3046,11 +3046,12 @@ class MyConverter implements Converter<String, Person> { … } - You can create both standard indexes and geospatial indexes using - the classes IndexDefinition and - GeoSpatialIndex respectfully. For example, given - the Venue class defined in a previous section, you would declare a - geospatial query as shown below + You can create standard, geospatial and text indexes using the + classes IndexDefinition, + GeoSpatialIndex and + TextIndexDefinition. For example, given the Venue + class defined in a previous section, you would declare a geospatial + query as shown below. mongoTemplate.indexOps(Venue.class).ensureIndex(new GeospatialIndex("location"));