diff --git a/driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java new file mode 100644 index 00000000000..0f18e2db7a9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java @@ -0,0 +1,53 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.client.model.search; + +import com.mongodb.internal.client.model.AbstractConstructibleBsonElement; + +import org.bson.conversions.Bson; + +import static com.mongodb.assertions.Assertions.notNull; + +final class PhraseConstructibleBsonElement extends AbstractConstructibleBsonElement implements + PhraseSearchOperator { + PhraseConstructibleBsonElement(final String name, final Bson value) { + super(name, value); + } + + private PhraseConstructibleBsonElement(final Bson baseElement, final Bson appendedElementValue) { + super(baseElement, appendedElementValue); + } + + @Override + protected PhraseConstructibleBsonElement newSelf(final Bson baseElement, final Bson appendedElementValue) { + return new PhraseConstructibleBsonElement(baseElement, appendedElementValue); + } + + @Override + public PhraseSearchOperator synonyms(final String name) { + return newWithAppendedValue("synonyms", notNull("name", name)); + } + + @Override + public PhraseSearchOperator slop(final int slop) { + return newWithAppendedValue("slop", slop); + } + + @Override + public PhraseConstructibleBsonElement score(final SearchScore modifier) { + return newWithAppendedValue("score", notNull("modifier", modifier)); + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java new file mode 100644 index 00000000000..3ac2abe05ad --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; + +/** + * @see SearchOperator#phrase(SearchPath, String) + * @see SearchOperator#phrase(Iterable, Iterable) + * @since 5.3 + */ + +@Sealed +@Beta(Reason.CLIENT) +public interface PhraseSearchOperator extends SearchOperator { + @Override + PhraseSearchOperator score(SearchScore modifier); + + /** + * Creates a new {@link PhraseSearchOperator} that uses slop. The default value is 0. + * + * @param slop The allowable distance between words in the query phrase. + * @return A new {@link PhraseSearchOperator}. + */ + PhraseSearchOperator slop(int slop); + + /** + * Creates a new {@link PhraseSearchOperator} that uses synonyms. + * + * @param name The name of the synonym mapping. + * @return A new {@link PhraseSearchOperator}. + * + * @mongodb.atlas.manual atlas-search/synonyms/ Synonym mappings + */ + PhraseSearchOperator synonyms(String name); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index 9234db91c51..00961bc3c18 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -292,6 +292,36 @@ static GeoNearSearchOperator near(final Point origin, final Number pivot, final .append("pivot", notNull("pivot", pivot))); } + /** + * Returns a {@link SearchOperator} that performs a search for documents containing an ordered sequence of terms. + * + * @param path The field to be searched. + * @param query The string to search for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/phrase/ phrase operator + */ + static PhraseSearchOperator phrase(final SearchPath path, final String query) { + return phrase(singleton(notNull("path", path)), singleton(notNull("query", query))); + } + + /** + * Returns a {@link SearchOperator} that performs a search for documents containing an ordered sequence of terms. + * + * @param paths The non-empty fields to be searched. + * @param queries The non-empty strings to search for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/phrase/ phrase operator + */ + static PhraseSearchOperator phrase(final Iterable paths, final Iterable queries) { + Iterator pathIterator = notNull("paths", paths).iterator(); + isTrueArgument("paths must not be empty", pathIterator.hasNext()); + Iterator queryIterator = notNull("queries", queries).iterator(); + isTrueArgument("queries must not be empty", queryIterator.hasNext()); + String firstQuery = queryIterator.next(); + return new PhraseConstructibleBsonElement("phrase", new Document("path", combineToBsonValue(pathIterator, false)) + .append("query", queryIterator.hasNext() ? queries : firstQuery)); + } + /** * Creates a {@link SearchOperator} from a {@link Bson} in situations when there is no builder method that better satisfies your needs. * This method cannot be used to validate the syntax. diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 29de80dda32..0dd8ab387d2 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -82,6 +82,7 @@ import static com.mongodb.client.model.search.SearchOperator.exists; import static com.mongodb.client.model.search.SearchOperator.near; import static com.mongodb.client.model.search.SearchOperator.numberRange; +import static com.mongodb.client.model.search.SearchOperator.phrase; import static com.mongodb.client.model.search.SearchOperator.text; import static com.mongodb.client.model.search.SearchOptions.searchOptions; import static com.mongodb.client.model.search.SearchPath.fieldPath; @@ -608,7 +609,8 @@ private static Stream searchAndSearchMetaArgs() { dateRange(fieldPath("fieldName6")) .lte(Instant.ofEpochMilli(1)), near(0, 1.5, fieldPath("fieldName7"), fieldPath("fieldName8")), - near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")) + near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), + phrase(fieldPath("fieldName10"), "term6") )) .minimumShouldMatch(1) .mustNot(singleton( diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index c0ea645fb73..1b34e8cf15c 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -581,6 +581,74 @@ void near() { ); } + @Test + void phrase() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // queries must not be empty + SearchOperator.phrase(singleton(fieldPath("fieldName")), emptyList()) + ), + () -> assertThrows(IllegalArgumentException.class, () -> + // paths must not be empty + SearchOperator.phrase(emptyList(), singleton("term")) + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + ), + SearchOperator.phrase( + fieldPath("fieldName"), + "term") + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", new BsonArray(asList( + fieldPath("fieldName").toBsonValue(), + wildcardPath("wildc*rd").toBsonValue()))) + .append("query", new BsonArray(asList( + new BsonString("term1"), + new BsonString("term2")))) + ), + SearchOperator.phrase( + asList( + fieldPath("fieldName"), + wildcardPath("wildc*rd")), + asList( + "term1", + "term2")) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + .append("synonyms", new BsonString("synonymMappingName")) + ), + SearchOperator.phrase( + singleton(fieldPath("fieldName")), + singleton("term")) + .synonyms("synonymMappingName") + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + .append("synonyms", new BsonString("synonymMappingName")) + .append("slop", new BsonInt32(5)) + ), + SearchOperator.phrase( + singleton(fieldPath("fieldName")), + singleton("term")) + .synonyms("synonymMappingName") + .slop(5) + .toBsonDocument() + ) + ); + } + private static SearchOperator docExamplePredefined() { return SearchOperator.exists( fieldPath("fieldName")); diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index 90f27092ebc..a72e5b3dbcc 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -228,6 +228,27 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that performs a search for documents containing an ordered sequence of terms. + * + * @param path The field to be searched. + * @param query The string to search for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/phrase/ phrase operator]] + */ + def phrase(path: SearchPath, query: String): PhraseSearchOperator = JSearchOperator.phrase(path, query) + + /** + * Returns a `SearchOperator` that performs a search for documents containing an ordered sequence of terms. + * + * @param paths The non-empty fields to be searched. + * @param queries The non-empty strings to search for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/phrase/ phrase operator]] + */ + def phrase(paths: Iterable[_ <: SearchPath], queries: Iterable[String]): PhraseSearchOperator = + JSearchOperator.phrase(paths.asJava, queries.asJava) + /** * Creates a `SearchOperator` from a `Bson` in situations when there is no builder method that better satisfies your needs. * This method cannot be used to validate the syntax. diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index 557060324cd..e9b86401471 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -119,6 +119,14 @@ package object search { @Beta(Array(Reason.CLIENT)) type TextSearchOperator = com.mongodb.client.model.search.TextSearchOperator + /** + * @see `SearchOperator.phrase(String, SearchPath)` + * @see `SearchOperator.phrase(Iterable, Iterable)` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type PhraseSearchOperator = com.mongodb.client.model.search.PhraseSearchOperator + /** * @see `SearchOperator.autocomplete(String, FieldSearchPath)` * @see `SearchOperator.autocomplete(Iterable, FieldSearchPath)`