diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
index d627ba2468..97cbfb536d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
@@ -19,6 +19,7 @@
 import java.util.Optional;
 import java.util.function.Function;
 
+import org.bson.conversions.Bson;
 import org.springframework.data.mongodb.core.mapping.Field;
 import org.springframework.data.mongodb.core.query.Collation;
 import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
@@ -41,6 +42,7 @@
  * @author Mark Paluch
  * @author Andreas Zink
  * @author Ben Foster
+ * @author Ross Lawley
  */
 public class CollectionOptions {
 
@@ -51,10 +53,11 @@ public class CollectionOptions {
 	private ValidationOptions validationOptions;
 	private @Nullable TimeSeriesOptions timeSeriesOptions;
 	private @Nullable CollectionChangeStreamOptions changeStreamOptions;
+	private @Nullable Bson encryptedFields;
 
 	private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
 			@Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions,
-			@Nullable CollectionChangeStreamOptions changeStreamOptions) {
+			@Nullable CollectionChangeStreamOptions changeStreamOptions,  @Nullable  Bson encryptedFields) {
 
 		this.maxDocuments = maxDocuments;
 		this.size = size;
@@ -63,6 +66,7 @@ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nul
 		this.validationOptions = validationOptions;
 		this.timeSeriesOptions = timeSeriesOptions;
 		this.changeStreamOptions = changeStreamOptions;
+		this.encryptedFields = encryptedFields;
 	}
 
 	/**
@@ -76,7 +80,7 @@ public static CollectionOptions just(Collation collation) {
 
 		Assert.notNull(collation, "Collation must not be null");
 
-		return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null);
+		return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null);
 	}
 
 	/**
@@ -86,7 +90,7 @@ public static CollectionOptions just(Collation collation) {
 	 * @since 2.0
 	 */
 	public static CollectionOptions empty() {
-		return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null);
+		return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null);
 	}
 
 	/**
@@ -136,7 +140,7 @@ public static CollectionOptions emitChangedRevisions() {
 	 */
 	public CollectionOptions capped() {
 		return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -148,7 +152,7 @@ public CollectionOptions capped() {
 	 */
 	public CollectionOptions maxDocuments(long maxDocuments) {
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -160,7 +164,7 @@ public CollectionOptions maxDocuments(long maxDocuments) {
 	 */
 	public CollectionOptions size(long size) {
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -172,7 +176,7 @@ public CollectionOptions size(long size) {
 	 */
 	public CollectionOptions collation(@Nullable Collation collation) {
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -293,7 +297,7 @@ public CollectionOptions validation(ValidationOptions validationOptions) {
 
 		Assert.notNull(validationOptions, "ValidationOptions must not be null");
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -307,7 +311,7 @@ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
 
 		Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -321,7 +325,19 @@ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStream
 
 		Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null");
 		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
-				changeStreamOptions);
+				changeStreamOptions, encryptedFields);
+	}
+
+	/**
+	 * Create new {@link CollectionOptions} with the given {@code encryptedFields}.
+	 *
+	 * @param encryptedFields can be null
+	 * @return new instance of {@link CollectionOptions}.
+	 * @since 4.5.0
+	 */
+	public CollectionOptions encryptedFields(@Nullable Bson encryptedFields) {
+		return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+				changeStreamOptions, encryptedFields);
 	}
 
 	/**
@@ -392,12 +408,22 @@ public Optional<CollectionChangeStreamOptions> getChangeStreamOptions() {
 		return Optional.ofNullable(changeStreamOptions);
 	}
 
+	/**
+	 * Get the {@code encryptedFields} if available.
+	 *
+	 * @return {@link Optional#empty()} if not specified.
+	 * @since 4.5.0
+	 */
+	public Optional<Bson> getEncryptedFields() {
+		return Optional.ofNullable(encryptedFields);
+	}
+
 	@Override
 	public String toString() {
 		return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped
 				+ ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions="
-				+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", disableValidation="
-				+ disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
+				+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedFields=" + encryptedFields
+				+ ", disableValidation=" + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
 				+ moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError="
 				+ failOnValidationError() + '}';
 	}
@@ -431,7 +457,10 @@ public boolean equals(@Nullable Object o) {
 		if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) {
 			return false;
 		}
-		return ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions);
+		if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) {
+			return false;
+		}
+		return ObjectUtils.nullSafeEquals(encryptedFields, that.encryptedFields);
 	}
 
 	@Override
@@ -443,6 +472,7 @@ public int hashCode() {
 		result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions);
 		result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions);
 		result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions);
+		result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFields);
 		return result;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
index f64391e8cd..601b6898b8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
@@ -19,11 +19,13 @@
  * Encryption algorithms supported by MongoDB Client Side Field Level Encryption.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 3.3
  */
 public final class EncryptionAlgorithms {
 
 	public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
 	public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
+	public static final String RANGE = "Range";
 
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
index 65a5131dd1..b7a2380ce9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
@@ -83,6 +83,7 @@
  * @author Mark Paluch
  * @author Christoph Strobl
  * @author Ben Foster
+ * @author Ross Lawley
  * @since 2.1
  * @see MongoTemplate
  * @see ReactiveMongoTemplate
@@ -378,6 +379,7 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
 		collectionOptions.getChangeStreamOptions().ifPresent(it -> result
 				.changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages())));
 
+		collectionOptions.getEncryptedFields().ifPresent(result::encryptedFields);
 		return result;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
index 5fde0acddd..acc8dfacb7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
@@ -28,29 +28,44 @@
  * {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 3.4
  */
 public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
 
 	private final PropertyValueProvider<MongoPersistentProperty> accessor; // TODO: generics
-	private final @Nullable MongoPersistentProperty persistentProperty;
 	private final MongoConverter mongoConverter;
 
+	@Nullable private final MongoPersistentProperty persistentProperty;
 	@Nullable private final SpELContext spELContext;
+	@Nullable private final String fieldNameAndQueryOperator;
 
 	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
 			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
-		this(accessor, persistentProperty, mongoConverter, null);
+		this(accessor, persistentProperty, mongoConverter, null, null);
 	}
 
 	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
 			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
 			@Nullable SpELContext spELContext) {
+		this(accessor, persistentProperty, mongoConverter, spELContext, null);
+	}
+
+	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
+			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
+			@Nullable String fieldNameAndQueryOperator) {
+		this(accessor, persistentProperty, mongoConverter, null, fieldNameAndQueryOperator);
+	}
+
+	public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
+			@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
+			@Nullable SpELContext spELContext, @Nullable String fieldNameAndQueryOperator) {
 
 		this.accessor = accessor;
 		this.persistentProperty = persistentProperty;
 		this.mongoConverter = mongoConverter;
 		this.spELContext = spELContext;
+		this.fieldNameAndQueryOperator = fieldNameAndQueryOperator;
 	}
 
 	@Override
@@ -84,4 +99,9 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
 	public SpELContext getSpELContext() {
 		return spELContext;
 	}
+
+	@Nullable
+	public String getFieldNameAndQueryOperator() {
+		return fieldNameAndQueryOperator;
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
index 39559b9979..45c036e8af 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
@@ -88,6 +88,7 @@
  * @author David Julia
  * @author Divya Srivastava
  * @author Gyungrai Wang
+ * @author Ross Lawley
  */
 public class QueryMapper {
 
@@ -670,9 +671,23 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu
 			PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter) {
 
 		MongoPersistentProperty property = documentField.getProperty();
+
+		String fieldNameAndQueryOperator = property != null && !property.getFieldName().equals(documentField.name)
+				? property.getFieldName() + "." + documentField.name
+				: documentField.name;
+
 		MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE,
-				property, converter);
+				property, converter, fieldNameAndQueryOperator);
+
+		return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext);
+	}
 
+	@Nullable
+	private Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value,
+			PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter,
+			MongoConversionContext conversionContext) {
+
+		MongoPersistentProperty property = documentField.getProperty();
 		/* might be an $in clause with multiple entries */
 		if (property != null && !property.isCollectionLike() && sourceValue instanceof Collection<?> collection) {
 
@@ -692,7 +707,10 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu
 
 			return BsonUtils.mapValues(document, (key, val) -> {
 				if (isKeyword(key)) {
-					return getMappedValue(documentField, val);
+					MongoConversionContext fieldConversionContext = new MongoConversionContext(
+							NoPropertyPropertyValueProvider.INSTANCE, property, converter,
+							conversionContext.getFieldNameAndQueryOperator() + "." + key);
+					return convertValueWithConversionContext(documentField, val, val, valueConverter, fieldConversionContext);
 				}
 				return val;
 			});
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
index f8d814fee4..e78feba732 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
@@ -26,6 +26,7 @@
  * Default {@link EncryptionContext} implementation.
  * 
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 class ExplicitEncryptionContext implements EncryptionContext {
@@ -66,4 +67,10 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
 	public <T> T write(@Nullable Object value, TypeInformation<T> target) {
 		return conversionContext.write(value, target);
 	}
+
+	@Override
+	@Nullable
+	public String getFieldNameAndQueryOperator() {
+		return conversionContext.getFieldNameAndQueryOperator();
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
index 1ce24b25fe..e3fdbe37cf 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
@@ -15,8 +15,14 @@
  */
 package org.springframework.data.mongodb.core.convert.encryption;
 
+import static java.util.Arrays.*;
+import static java.util.Collections.*;
+import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
+import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*;
+
 import java.util.Collection;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.logging.Log;
@@ -31,9 +37,11 @@
 import org.springframework.data.mongodb.core.convert.MongoConversionContext;
 import org.springframework.data.mongodb.core.encryption.Encryption;
 import org.springframework.data.mongodb.core.encryption.EncryptionContext;
+import org.springframework.data.mongodb.core.encryption.EncryptionKey;
 import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver;
 import org.springframework.data.mongodb.core.encryption.EncryptionOptions;
 import org.springframework.data.mongodb.core.mapping.Encrypted;
+import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
 import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
 import org.springframework.data.mongodb.util.BsonUtils;
 import org.springframework.lang.Nullable;
@@ -44,11 +52,14 @@
  * {@link Encrypted @Encrypted} to provide key and algorithm metadata.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public class MongoEncryptionConverter implements EncryptingConverter<Object, Object> {
 
 	private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
+	private static final String EQUALITY_OPERATOR = "$eq";
+	private static final List<String> RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte");
 
 	private final Encryption<BsonValue, BsonBinary> encryption;
 	private final EncryptionKeyResolver keyResolver;
@@ -161,8 +172,42 @@ public Object encrypt(Object value, EncryptionContext context) {
 					getProperty(context).getOwner().getName(), getProperty(context).getName()));
 		}
 
-		EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context));
+		boolean encryptExpression = false;
+		String algorithm = annotation.algorithm();
+		EncryptionKey key = keyResolver.getKey(context);
+		EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key);
+		String fieldNameAndQueryOperator = context.getFieldNameAndQueryOperator();
+
+		ExplicitEncrypted explicitEncryptedAnnotation = persistentProperty.findAnnotation(ExplicitEncrypted.class);
+		if (explicitEncryptedAnnotation != null) {
+			QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none();
+			String rangeOptions = explicitEncryptedAnnotation.rangeOptions();
+			if (!rangeOptions.isEmpty()) {
+				queryableEncryptionOptions = queryableEncryptionOptions.rangeOptions(Document.parse(rangeOptions));
+			}
 
+			if (explicitEncryptedAnnotation.contentionFactor() >= 0) {
+				queryableEncryptionOptions = queryableEncryptionOptions
+						.contentionFactor(explicitEncryptedAnnotation.contentionFactor());
+			}
+
+			boolean isPartOfARangeQuery = algorithm.equalsIgnoreCase(RANGE) && fieldNameAndQueryOperator != null;
+			if (isPartOfARangeQuery) {
+				encryptExpression = true;
+				queryableEncryptionOptions = queryableEncryptionOptions.queryType("range");
+			}
+			encryptionOptions = new EncryptionOptions(algorithm, key, queryableEncryptionOptions);
+		}
+
+		if (encryptExpression) {
+			return encryptExpression(fieldNameAndQueryOperator, value, encryptionOptions);
+		} else {
+			return encryptValue(value, context, persistentProperty, encryptionOptions);
+		}
+	}
+
+	private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPersistentProperty persistentProperty,
+			EncryptionOptions encryptionOptions) {
 		if (!persistentProperty.isEntity()) {
 
 			if (persistentProperty.isCollectionLike()) {
@@ -187,6 +232,42 @@ public Object encrypt(Object value, EncryptionContext context) {
 		return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions);
 	}
 
+	/**
+	 * Encrypts a range query expression.
+	 *
+	 * <p>The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method
+	 * ensures these requirements are met and then picks out and returns just the value for use with a range query.
+	 *
+	 * @param fieldNameAndQueryOperator field name and query operator
+	 * @param value the value of the expression to be encrypted
+	 * @param encryptionOptions the options
+	 * @return the encrypted range value for use in a range query
+	 */
+	private BsonValue encryptExpression(String fieldNameAndQueryOperator, Object value,
+			EncryptionOptions encryptionOptions) {
+		BsonValue doc = BsonUtils.simpleToBsonValue(value);
+
+		String fieldName = fieldNameAndQueryOperator;
+		String queryOperator = EQUALITY_OPERATOR;
+
+		int pos = fieldNameAndQueryOperator.lastIndexOf(".$");
+		if (pos > -1) {
+			fieldName = fieldNameAndQueryOperator.substring(0, pos);
+			queryOperator = fieldNameAndQueryOperator.substring(pos + 1);
+		}
+
+		if (!RANGE_OPERATORS.contains(queryOperator)) {
+			throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the "
+					+ "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName));
+		}
+
+		BsonDocument encryptExpression = new BsonDocument("$and",
+				new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc)))));
+
+		BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions);
+		return result.getArray("$and").get(0).asDocument().getDocument(fieldName).getBinary(queryOperator);
+	}
+
 	private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
 			EncryptionContext context) {
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
index 5645c1e416..16202598f5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
@@ -15,10 +15,13 @@
  */
 package org.springframework.data.mongodb.core.encryption;
 
+import org.bson.BsonDocument;
+
 /**
  * Component responsible for encrypting and decrypting values.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public interface Encryption<S, T> {
@@ -40,4 +43,16 @@ public interface Encryption<S, T> {
 	 */
 	S decrypt(T value);
 
+	/**
+	 * Encrypt the given expression.
+	 *
+	 * @param value must not be {@literal null}.
+	 * @param options must not be {@literal null}.
+	 * @return the encrypted expression.
+	 * @since 4.5.0
+	 */
+	default BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) {
+		throw new UnsupportedOperationException("Unsupported encryption method");
+	}
+
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
index 89beaadedb..1643e2f950 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
@@ -25,6 +25,7 @@
  * Context to encapsulate encryption for a specific {@link MongoPersistentProperty}.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public interface EncryptionContext {
@@ -128,4 +129,13 @@ default <T> T write(@Nullable Object value, Class<T> target) {
 
 	EvaluationContext getEvaluationContext(Object source);
 
+	/**
+	 * The field name and field query operator
+	 *
+	 * @return can be {@literal null}.
+	 */
+	@Nullable
+	default String getFieldNameAndQueryOperator() {
+		return null;
+	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
index fe01cfa8ba..5affbeddb1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
@@ -15,6 +15,15 @@
  */
 package org.springframework.data.mongodb.core.encryption;
 
+import java.util.Objects;
+import java.util.Optional;
+
+import com.mongodb.client.model.vault.RangeOptions;
+import org.bson.Document;
+import org.springframework.data.mongodb.core.FindAndReplaceOptions;
+import org.springframework.data.mongodb.util.BsonUtils;
+import org.springframework.data.util.Optionals;
+import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
@@ -22,20 +31,27 @@
  * Options, like the {@link #algorithm()}, to apply when encrypting values.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public class EncryptionOptions {
 
 	private final String algorithm;
 	private final EncryptionKey key;
+	private final QueryableEncryptionOptions queryableEncryptionOptions;
 
 	public EncryptionOptions(String algorithm, EncryptionKey key) {
+		this(algorithm, key, QueryableEncryptionOptions.NONE);
+	}
 
+	public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) {
 		Assert.hasText(algorithm, "Algorithm must not be empty");
 		Assert.notNull(key, "EncryptionKey must not be empty");
+		Assert.notNull(key, "QueryableEncryptionOptions must not be empty");
 
 		this.key = key;
 		this.algorithm = algorithm;
+		this.queryableEncryptionOptions = queryableEncryptionOptions;
 	}
 
 	public EncryptionKey key() {
@@ -46,6 +62,10 @@ public String algorithm() {
 		return algorithm;
 	}
 
+	public QueryableEncryptionOptions queryableEncryptionOptions() {
+		return queryableEncryptionOptions;
+	}
+
 	@Override
 	public boolean equals(Object o) {
 
@@ -61,7 +81,11 @@ public boolean equals(Object o) {
 		if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) {
 			return false;
 		}
-		return ObjectUtils.nullSafeEquals(key, that.key);
+		if (!ObjectUtils.nullSafeEquals(key, that.key)) {
+			return false;
+		}
+
+		return ObjectUtils.nullSafeEquals(queryableEncryptionOptions, that.queryableEncryptionOptions);
 	}
 
 	@Override
@@ -69,11 +93,170 @@ public int hashCode() {
 
 		int result = ObjectUtils.nullSafeHashCode(algorithm);
 		result = 31 * result + ObjectUtils.nullSafeHashCode(key);
+		result = 31 * result + ObjectUtils.nullSafeHashCode(queryableEncryptionOptions);
 		return result;
 	}
 
 	@Override
 	public String toString() {
-		return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
+		return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + ", queryableEncryptionOptions='"
+				+ queryableEncryptionOptions + "'}";
+	}
+
+	/**
+	 * Options, like the {@link #getQueryType()}, to apply when encrypting queryable values.
+	 *
+	 * @author Ross Lawley
+	 */
+	public static class QueryableEncryptionOptions {
+
+		private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, null);
+
+		private final @Nullable String queryType;
+		private final @Nullable Long contentionFactor;
+		private final @Nullable Document rangeOptions;
+
+		private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor,
+				@Nullable Document rangeOptions) {
+			this.queryType = queryType;
+			this.contentionFactor = contentionFactor;
+			this.rangeOptions = rangeOptions;
+		}
+
+		/**
+		 * Create an empty {@link QueryableEncryptionOptions}.
+		 *
+		 * @return unmodifiable {@link QueryableEncryptionOptions} instance.
+		 */
+		public static QueryableEncryptionOptions none() {
+			return NONE;
+		}
+
+		/**
+		 * Define the {@code queryType} to be used for queryable document encryption.
+		 *
+		 * @param queryType can be {@literal null}.
+		 * @return new instance of {@link QueryableEncryptionOptions}.
+		 */
+		public QueryableEncryptionOptions queryType(@Nullable String queryType) {
+			return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
+		}
+
+		/**
+		 * Define the {@code contentionFactor} to be used for queryable document encryption.
+		 *
+		 * @param contentionFactor can be {@literal null}.
+		 * @return new instance of {@link QueryableEncryptionOptions}.
+		 */
+		public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) {
+			return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
+		}
+
+		/**
+		 * Define the {@code rangeOptions} to be used for queryable document encryption.
+		 *
+		 * @param rangeOptions can be {@literal null}.
+		 * @return new instance of {@link QueryableEncryptionOptions}.
+		 */
+		public QueryableEncryptionOptions rangeOptions(@Nullable Document rangeOptions) {
+			return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
+		}
+
+		/**
+		 * Get the {@code queryType} to apply.
+		 *
+		 * @return {@link Optional#empty()} if not set.
+		 */
+		public Optional<String> getQueryType() {
+			return Optional.ofNullable(queryType);
+		}
+
+		/**
+		 * Get the {@code contentionFactor} to apply.
+		 *
+		 * @return {@link Optional#empty()} if not set.
+		 */
+		public Optional<Long> getContentionFactor() {
+			return Optional.ofNullable(contentionFactor);
+		}
+
+		/**
+		 * Get the {@code rangeOptions} to apply.
+		 *
+		 * @return {@link Optional#empty()} if not set.
+		 */
+		public Optional<RangeOptions> getRangeOptions() {
+			if (rangeOptions == null) {
+				return Optional.empty();
+			}
+			RangeOptions encryptionRangeOptions = new RangeOptions();
+
+			if (rangeOptions.containsKey("min")) {
+				encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(rangeOptions.get("min")));
+			}
+			if (rangeOptions.containsKey("max")) {
+				encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(rangeOptions.get("max")));
+			}
+			if (rangeOptions.containsKey("trimFactor")) {
+				Object trimFactor = rangeOptions.get("trimFactor");
+				Assert.isInstanceOf(Integer.class, trimFactor, () -> String
+						.format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass()));
+
+				encryptionRangeOptions.trimFactor((Integer) trimFactor);
+			}
+
+			if (rangeOptions.containsKey("sparsity")) {
+				Object sparsity = rangeOptions.get("sparsity");
+				Assert.isInstanceOf(Number.class, sparsity,
+						() -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass()));
+				encryptionRangeOptions.sparsity(((Number) sparsity).longValue());
+			}
+
+			if (rangeOptions.containsKey("precision")) {
+				Object precision = rangeOptions.get("precision");
+				Assert.isInstanceOf(Number.class, precision, () -> String
+						.format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass()));
+				encryptionRangeOptions.precision(((Number) precision).intValue());
+			}
+			return Optional.of(encryptionRangeOptions);
+		}
+
+		/**
+		 * @return {@literal true} if no arguments set.
+		 */
+		boolean isEmpty() {
+			return !Optionals.isAnyPresent(getQueryType(), getContentionFactor(), getRangeOptions());
+		}
+
+		@Override
+		public String toString() {
+			return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor
+					+ ", rangeOptions=" + rangeOptions + '}';
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o) {
+				return true;
+			}
+			if (o == null || getClass() != o.getClass()) {
+				return false;
+			}
+			QueryableEncryptionOptions that = (QueryableEncryptionOptions) o;
+
+			if (!ObjectUtils.nullSafeEquals(queryType, that.queryType)) {
+				return false;
+			}
+
+			if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) {
+				return false;
+			}
+			return ObjectUtils.nullSafeEquals(rangeOptions, that.rangeOptions);
+		}
+
+		@Override
+		public int hashCode() {
+			return Objects.hash(queryType, contentionFactor, rangeOptions);
+		}
 	}
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
index 92350ce7d7..4d250fba05 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
@@ -18,6 +18,7 @@
 import java.util.function.Supplier;
 
 import org.bson.BsonBinary;
+import org.bson.BsonDocument;
 import org.bson.BsonValue;
 import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type;
 import org.springframework.util.Assert;
@@ -29,6 +30,7 @@
  * {@link ClientEncryption} based {@link Encryption} implementation.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  */
 public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> {
@@ -59,7 +61,19 @@ public BsonValue decrypt(BsonBinary value) {
 
 	@Override
 	public BsonBinary encrypt(BsonValue value, EncryptionOptions options) {
+		return getClientEncryption().encrypt(value, createEncryptOptions(options));
+	}
+
+	@Override
+	public BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) {
+		return getClientEncryption().encryptExpression(value, createEncryptOptions(options));
+	}
+
+	public ClientEncryption getClientEncryption() {
+		return source.get();
+	}
 
+	private EncryptOptions createEncryptOptions(EncryptionOptions options) {
 		EncryptOptions encryptOptions = new EncryptOptions(options.algorithm());
 
 		if (Type.ALT.equals(options.key().type())) {
@@ -68,11 +82,10 @@ public BsonBinary encrypt(BsonValue value, EncryptionOptions options) {
 			encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value());
 		}
 
-		return getClientEncryption().encrypt(value, encryptOptions);
-	}
-
-	public ClientEncryption getClientEncryption() {
-		return source.get();
+		options.queryableEncryptionOptions().getQueryType().map(encryptOptions::queryType);
+		options.queryableEncryptionOptions().getContentionFactor().map(encryptOptions::contentionFactor);
+		options.queryableEncryptionOptions().getRangeOptions().map(encryptOptions::rangeOptions);
+		return encryptOptions;
 	}
 
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
index 5f08e5c787..a8aedce8bc 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
@@ -47,6 +47,7 @@
  * </pre>
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.1
  * @see ValueConverter
  */
@@ -60,7 +61,8 @@
 	 * Define the algorithm to use.
 	 * <p>
 	 * A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a
-	 * {@literal randomized} one will produce different results every time.
+	 * {@literal randomized} one will produce different results every time.  A {@literal range} algorithm allows for
+	 * the value to be queried whilst encrypted.
 	 * <p>
 	 * Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex
 	 * objects and arrays as well as the query limitations that come with each of them.
@@ -84,6 +86,24 @@
 	 */
 	String keyAltName() default "";
 
+	/**
+	 * Set the contention factor
+	 * <p>
+	 * Only required when using {@literal range} encryption.
+	 * @return the contention factor
+	 */
+	long contentionFactor() default -1;
+
+	/**
+	 * Set the {@literal range} options
+	 * <p>
+	 * Should be valid extended json representing the range options and including the following values:
+	 * {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}.
+	 *
+	 * @return the json representation of range options
+	 */
+	String rangeOptions() default "";
+
 	/**
 	 * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property.
 	 *
@@ -91,4 +111,5 @@
 	 */
 	@AliasFor(annotation = ValueConverter.class, value = "value")
 	Class<? extends PropertyValueConverter> value() default MongoEncryptionConverter.class;
+
 }
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
index f85be98c1f..b17b9f1963 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
@@ -34,6 +34,7 @@
 import com.mongodb.client.MongoDatabase;
 import com.mongodb.client.MongoIterable;
 import com.mongodb.client.model.IndexOptions;
+import com.mongodb.client.model.vault.RangeOptions;
 import com.mongodb.reactivestreams.client.MapReducePublisher;
 
 /**
@@ -42,11 +43,13 @@
  * This class is for internal use within the framework and should not be used by applications.
  *
  * @author Christoph Strobl
+ * @author Ross Lawley
  * @since 4.3
  */
 public class MongoCompatibilityAdapter {
 
 	private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer";
+	private static final String NOT_SUPPORTED_ON_4 = "%s is not supported on Mongo Client 4";
 
 	private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class,
 			"getStreamFactoryFactory");
@@ -54,6 +57,9 @@ public class MongoCompatibilityAdapter {
 	private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize",
 			Double.class);
 
+	private static final @Nullable Method setTrimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor",
+			Integer.class);
+
 	/**
 	 * Return a compatibility adapter for {@link MongoClientSettings.Builder}.
 	 *
@@ -199,6 +205,10 @@ public interface MongoDatabaseAdapterBuilder {
 		MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db);
 	}
 
+	public interface RangeOptionsAdapter {
+		void trimFactor(Integer trimFactor);
+	}
+
 	@SuppressWarnings({ "unchecked", "DataFlowIssue" })
 	public static class MongoDatabaseAdapter {
 
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
new file mode 100644
index 0000000000..2c5e3abc6b
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright 2023-2024 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
+ *
+ *      https://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.encryption;
+
+import static java.util.Arrays.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
+import static org.springframework.data.mongodb.core.query.Criteria.*;
+
+import java.security.SecureRandom;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import com.mongodb.AutoEncryptionSettings;
+import com.mongodb.ClientEncryptionSettings;
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoNamespace;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.CreateCollectionOptions;
+import com.mongodb.client.model.CreateEncryptedCollectionParams;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.IndexOptions;
+import com.mongodb.client.model.Indexes;
+import com.mongodb.client.vault.ClientEncryption;
+import com.mongodb.client.vault.ClientEncryptions;
+
+import org.bson.BsonArray;
+import org.bson.BsonBinary;
+import org.bson.BsonDocument;
+import org.bson.BsonInt32;
+import org.bson.BsonInt64;
+import org.bson.BsonNull;
+import org.bson.BsonString;
+import org.bson.BsonValue;
+import org.bson.Document;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.convert.PropertyValueConverterFactory;
+import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
+import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
+import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
+import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
+import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
+import org.springframework.data.mongodb.test.util.MongoClientExtension;
+import org.springframework.data.util.Lazy;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+/**
+ * @author Ross Lawley
+ */
+@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
+@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0")
+@EnableIfReplicaSetAvailable
+@ContextConfiguration(classes = RangeEncryptionTests.EncryptionConfig.class)
+class RangeEncryptionTests {
+
+	@Autowired MongoTemplate template;
+
+	@AfterEach
+	void tearDown() {
+		template.getDb().getCollection("test").deleteMany(new BsonDocument());
+	}
+
+	@Test
+	void canGreaterThanEqualMatchRangeEncryptedField() {
+		Person source = createPerson();
+		template.insert(source);
+
+		Person loaded = template.query(Person.class).matching(where("encryptedInt").gte(source.encryptedInt)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test
+	void canLesserThanEqualMatchRangeEncryptedField() {
+		Person source = createPerson();
+		template.insert(source);
+
+		Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test
+	void canRangeMatchRangeEncryptedField() {
+		Person source = createPerson();
+		template.insert(source);
+
+		Person loaded = template.query(Person.class).matching(where("encryptedLong").lte(1001L).gte(1001L)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test
+	void canUpdateRangeEncryptedField() {
+		Person source = createPerson();
+		template.insert(source);
+
+		source.encryptedInt = 123;
+		source.encryptedLong = 9999L;
+		template.save(source);
+
+		Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue();
+		assertThat(loaded).isEqualTo(source);
+	}
+
+	@Test
+	void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() {
+		Person source = createPerson();
+		template.insert(source);
+
+		assertThatThrownBy(
+				() -> template.query(Person.class).matching(where("encryptedInt").is(source.encryptedInt)).firstValue())
+				.isInstanceOf(AssertionError.class)
+				.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+						+ "the query operator '$eq' for field path 'encryptedInt' is not a range query.");
+
+	}
+
+	@Test
+	void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() {
+		Person source = createPerson();
+		template.insert(source);
+
+		assertThatThrownBy(
+				() -> template.query(Person.class).matching(where("encryptedLong").in(1001L, 9999L)).firstValue())
+				.isInstanceOf(AssertionError.class)
+				.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+						+ "the query operator '$in' for field path 'encryptedLong' is not a range query.");
+
+	}
+
+	private Person createPerson() {
+		Person source = new Person();
+		source.id = "id-1";
+		source.encryptedInt = 101;
+		source.encryptedLong = 1001L;
+		return source;
+	}
+
+	protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
+
+		private static final String LOCAL_KMS_PROVIDER = "local";
+
+		private static final Lazy<Map<String, Map<String, Object>>> LAZY_KMS_PROVIDERS = Lazy.of(() -> {
+			byte[] localMasterKey = new byte[96];
+			new SecureRandom().nextBytes(localMasterKey);
+			return Map.of(LOCAL_KMS_PROVIDER, Map.of("key", localMasterKey));
+		});
+
+		@Autowired ApplicationContext applicationContext;
+
+		@Override
+		protected String getDatabaseName() {
+			return "qe-test";
+		}
+
+		@Bean
+		public MongoClient mongoClient() {
+			return super.mongoClient();
+		}
+
+		@Override
+		protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
+			converterConfigurationAdapter
+					.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext))
+					.useNativeDriverJavaTimeCodecs();
+		}
+
+		@Bean
+		MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
+			Lazy<Map<String, BsonBinary>> lazyDataKeyMap = Lazy.of(() -> {
+				try (MongoClient client = mongoClient()) {
+					MongoDatabase database = client.getDatabase(getDatabaseName());
+					database.getCollection("test").drop();
+
+					ClientEncryption clientEncryption = mongoClientEncryption.getClientEncryption();
+					BsonDocument encryptedFields = new BsonDocument().append("fields",
+							new BsonArray(asList(
+									new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedInt"))
+											.append("bsonType", new BsonString("int"))
+											.append("queries",
+													new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L))
+															.append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1))
+															.append("min", new BsonInt32(0)).append("max", new BsonInt32(200))),
+									new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedLong"))
+											.append("bsonType", new BsonString("long")).append("queries",
+													new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L))
+															.append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1))
+															.append("min", new BsonInt64(1000)).append("max", new BsonInt64(9999))))));
+
+					BsonDocument local = clientEncryption.createEncryptedCollection(database, "test",
+							new CreateCollectionOptions().encryptedFields(encryptedFields),
+							new CreateEncryptedCollectionParams(LOCAL_KMS_PROVIDER));
+
+					return local.getArray("fields").stream().map(BsonValue::asDocument).collect(
+							Collectors.toMap(field -> field.getString("path").getValue(), field -> field.getBinary("keyId")));
+				}
+			});
+			return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver
+					.annotated((ctx) -> EncryptionKey.keyId(lazyDataKeyMap.get().get(ctx.getProperty().getFieldName()))));
+		}
+
+		@Bean
+		CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
+			return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
+		}
+
+		@Override
+		protected void configureClientSettings(MongoClientSettings.Builder builder) {
+			try (MongoClient client = MongoClients.create()) {
+				ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(client);
+
+				builder.autoEncryptionSettings(AutoEncryptionSettings.builder() //
+						.kmsProviders(clientEncryptionSettings.getKmsProviders()) //
+						.keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) //
+						.bypassQueryAnalysis(true).build());
+			}
+		}
+
+		@Bean
+		ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) {
+			MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault");
+			MongoCollection<Document> keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName())
+					.getCollection(keyVaultNamespace.getCollectionName());
+			keyVaultCollection.drop();
+			// Ensure that two data keys cannot share the same keyAltName.
+			keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"),
+					new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames")));
+
+			mongoClient.getDatabase(getDatabaseName()).getCollection("test").drop(); // Clear old data
+
+			// Create the ClientEncryption instance
+			return ClientEncryptionSettings.builder() //
+					.keyVaultMongoClientSettings(
+							MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
+					.keyVaultNamespace(keyVaultNamespace.getFullName()) //
+					.kmsProviders(LAZY_KMS_PROVIDERS.get()) //
+					.build();
+		}
+	}
+
+	static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
+
+		static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
+
+		CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
+			super(() -> {
+				ClientEncryption clientEncryption = cache.get();
+				if (clientEncryption == null) {
+					clientEncryption = source.get();
+					cache.set(clientEncryption);
+				}
+
+				return clientEncryption;
+			});
+		}
+
+		@Override
+		public void destroy() {
+			ClientEncryption clientEncryption = cache.get();
+			if (clientEncryption != null) {
+				clientEncryption.close();
+				cache.set(null);
+			}
+		}
+	}
+
+	@org.springframework.data.mongodb.core.mapping.Document("test")
+	static class Person {
+
+		String id;
+		String name;
+
+		@ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L,
+				rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") Integer encryptedInt;
+		@ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L,
+				rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") Long encryptedLong;
+
+		public String getId() {
+			return this.id;
+		}
+
+		public void setId(String id) {
+			this.id = id;
+		}
+
+		public String getName() {
+			return this.name;
+		}
+
+		public void setName(String name) {
+			this.name = name;
+		}
+
+		public Integer getEncryptedInt() {
+			return this.encryptedInt;
+		}
+
+		public void setEncryptedInt(Integer encryptedInt) {
+			this.encryptedInt = encryptedInt;
+		}
+
+		public Long getEncryptedLong() {
+			return this.encryptedLong;
+		}
+
+		public void setEncryptedLong(Long encryptedLong) {
+			this.encryptedLong = encryptedLong;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (this == o)
+				return true;
+			if (o == null || getClass() != o.getClass())
+				return false;
+
+			Person person = (Person) o;
+			return Objects.equals(id, person.id) && Objects.equals(name, person.name)
+					&& Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong);
+		}
+
+		@Override
+		public int hashCode() {
+			int result = Objects.hashCode(id);
+			result = 31 * result + Objects.hashCode(name);
+			result = 31 * result + Objects.hashCode(encryptedInt);
+			result = 31 * result + Objects.hashCode(encryptedLong);
+			return result;
+		}
+
+		@Override
+		public String toString() {
+			return "Person{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", encryptedInt=" + encryptedInt
+					+ ", encryptedLong=" + encryptedLong + '}';
+		}
+	}
+
+}