From d2731919f86b8d1e887b74031d1f961714302072 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 23 Jul 2026 14:38:10 +0100 Subject: [PATCH 1/7] JAVA-5987: support $vectorSearch against nested embeddings and arrays of embeddings Add query-time $vectorSearch options for nested (embedded) and arrays-of embeddings: - VectorSearchOptions.parentFilter(Bson): parent/root-level filter for a nested search (the existing filter(Bson) targets the leaf/embedded docs). - VectorSearchOptions.nestedOptions(VectorSearchNestedOptions): the nestedOptions sub-document. - VectorSearchNestedOptions: an extensible @Sealed options type with a typed scoreMode(VectorSearchScoreMode) helper and an option(String, Object) escape hatch, so future server fields are additive. - VectorSearchScoreMode enum (avg/max) for embedded score aggregation. Follows the existing self-rendering immutable options-builder pattern; no path introspection. Index-time nesting needs no new API (SearchIndexModel accepts a raw Bson definition). --- .../com/mongodb/client/model/Aggregates.java | 9 ++ .../search/VectorSearchConstructibleBson.java | 10 ++ .../VectorSearchNestedConstructibleBson.java | 56 +++++++++ .../search/VectorSearchNestedOptions.java | 69 +++++++++++ .../model/search/VectorSearchOptions.java | 38 ++++++ .../model/search/VectorSearchScoreMode.java | 54 +++++++++ .../search/VectorSearchNestedOptionsTest.java | 108 ++++++++++++++++++ .../model/search/VectorSearchOptionsTest.java | 81 +++++++++++++ .../search/VectorSearchNestedOptions.scala | 35 ++++++ .../model/search/VectorSearchScoreMode.scala | 38 ++++++ .../mongodb/scala/model/search/package.scala | 18 +++ .../scala/ApiAliasAndCompanionSpec.scala | 16 +-- 12 files changed, 525 insertions(+), 7 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedConstructibleBson.java create mode 100644 driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedOptions.java create mode 100644 driver-core/src/main/com/mongodb/client/model/search/VectorSearchScoreMode.java create mode 100644 driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchNestedOptionsTest.java create mode 100644 driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java create mode 100644 driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchNestedOptions.scala create mode 100644 driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchScoreMode.scala diff --git a/driver-core/src/main/com/mongodb/client/model/Aggregates.java b/driver-core/src/main/com/mongodb/client/model/Aggregates.java index 29531e76e16..328898e53ee 100644 --- a/driver-core/src/main/com/mongodb/client/model/Aggregates.java +++ b/driver-core/src/main/com/mongodb/client/model/Aggregates.java @@ -28,8 +28,10 @@ import com.mongodb.client.model.search.SearchOperator; import com.mongodb.client.model.search.SearchOptions; import com.mongodb.client.model.search.TextVectorSearchQuery; +import com.mongodb.client.model.search.VectorSearchNestedOptions; import com.mongodb.client.model.search.VectorSearchOptions; import com.mongodb.client.model.search.VectorSearchQuery; +import com.mongodb.client.model.search.VectorSearchScoreMode; import com.mongodb.lang.Nullable; import org.bson.BsonArray; import org.bson.BsonBoolean; @@ -951,6 +953,13 @@ public static Bson searchMeta(final SearchCollector collector, final SearchOptio * You may use the {@code $meta: "vectorSearchScore"} expression, e.g., via {@link Projections#metaVectorSearchScore(String)}, * to extract the relevance score assigned to each found document. * + *

The {@code path} may reference a nested (embedded) field using dot notation, in which case the + * search is performed against embeddings in the embedded documents. For a nested search, use + * {@link VectorSearchOptions#filter(Bson)} to filter the leaf (embedded) documents, + * {@link VectorSearchOptions#parentFilter(Bson)} to filter the parent documents, and + * {@link VectorSearchOptions#nestedOptions(VectorSearchNestedOptions)} (e.g. + * {@link VectorSearchNestedOptions#scoreMode(VectorSearchScoreMode)}) to control score aggregation.

+ * * @param queryVector The query vector. The number of dimensions must match that of the {@code index}. * @param path The field to be searched. * @param index The name of the index to use. diff --git a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchConstructibleBson.java b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchConstructibleBson.java index 39a043ca82f..38cf0af0b05 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchConstructibleBson.java +++ b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchConstructibleBson.java @@ -49,6 +49,16 @@ public VectorSearchOptions filter(final Bson filter) { return newAppended("filter", notNull("filter", filter)); } + @Override + public VectorSearchOptions parentFilter(final Bson parentFilter) { + return newAppended("parentFilter", notNull("parentFilter", parentFilter)); + } + + @Override + public VectorSearchOptions nestedOptions(final VectorSearchNestedOptions nestedOptions) { + return newAppended("nestedOptions", notNull("nestedOptions", nestedOptions)); + } + @Override public VectorSearchOptions returnStoredSource(final boolean returnStoredSource) { return newAppended("returnStoredSource", new BsonBoolean(returnStoredSource)); diff --git a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedConstructibleBson.java b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedConstructibleBson.java new file mode 100644 index 00000000000..f0b5966f87d --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedConstructibleBson.java @@ -0,0 +1,56 @@ +/* + * 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.Immutable; +import com.mongodb.internal.client.model.AbstractConstructibleBson; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.conversions.Bson; + +import static com.mongodb.assertions.Assertions.notNull; + +final class VectorSearchNestedConstructibleBson extends AbstractConstructibleBson + implements VectorSearchNestedOptions { + /** + * An {@linkplain Immutable immutable} {@link BsonDocument#isEmpty() empty} instance. + */ + static final VectorSearchNestedConstructibleBson EMPTY_IMMUTABLE = + new VectorSearchNestedConstructibleBson(AbstractConstructibleBson.EMPTY_IMMUTABLE); + + VectorSearchNestedConstructibleBson(final Bson base) { + super(base); + } + + private VectorSearchNestedConstructibleBson(final Bson base, final Document appended) { + super(base, appended); + } + + @Override + protected VectorSearchNestedConstructibleBson newSelf(final Bson base, final Document appended) { + return new VectorSearchNestedConstructibleBson(base, appended); + } + + @Override + public VectorSearchNestedOptions scoreMode(final VectorSearchScoreMode scoreMode) { + return newAppended("scoreMode", notNull("scoreMode", scoreMode).getValue()); + } + + @Override + public VectorSearchNestedOptions option(final String name, final Object value) { + return newAppended(notNull("name", name), notNull("value", value)); + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedOptions.java b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedOptions.java new file mode 100644 index 00000000000..cc0d5c16314 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchNestedOptions.java @@ -0,0 +1,69 @@ +/* + * 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.Sealed; +import org.bson.conversions.Bson; + +/** + * Represents the optional {@code nestedOptions} sub-document of the {@code $vectorSearch} pipeline stage, + * used when searching against arrays of embeddings within nested (embedded) documents. + * + * @see VectorSearchOptions#nestedOptions(VectorSearchNestedOptions) + * @mongodb.atlas.manual atlas-vector-search/vector-search-stage/ $vectorSearch + * @mongodb.server.release 8.3 + * @since 5.10 + */ +@Sealed +public interface VectorSearchNestedOptions extends Bson { + /** + * Creates a new {@link VectorSearchNestedOptions} with the score aggregation mode specified. + * + * @param scoreMode The score aggregation mode for the matching embeddings within a document. + * @return A new {@link VectorSearchNestedOptions}. + */ + VectorSearchNestedOptions scoreMode(VectorSearchScoreMode scoreMode); + + /** + * Creates a new {@link VectorSearchNestedOptions} with the specified option in situations when there is no + * builder method that better satisfies your needs. + * This method cannot be used to validate the syntax. + *

+ * Example
+ * The following code creates two functionally equivalent {@link VectorSearchNestedOptions} objects, + * though they may not be {@linkplain Object#equals(Object) equal}. + *

{@code
+     *  VectorSearchNestedOptions options1 = VectorSearchNestedOptions.vectorSearchNestedOptions()
+     *      .scoreMode(VectorSearchScoreMode.AVG);
+     *  VectorSearchNestedOptions options2 = VectorSearchNestedOptions.vectorSearchNestedOptions()
+     *      .option("scoreMode", "avg");
+     * }
+ * + * @param name The option name. + * @param value The option value. + * @return A new {@link VectorSearchNestedOptions}. + */ + VectorSearchNestedOptions option(String name, Object value); + + /** + * Returns {@link VectorSearchNestedOptions} that represents server defaults. + * + * @return {@link VectorSearchNestedOptions} that represents server defaults. + */ + static VectorSearchNestedOptions vectorSearchNestedOptions() { + return VectorSearchNestedConstructibleBson.EMPTY_IMMUTABLE; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchOptions.java b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchOptions.java index d3bcf3aea46..d52cafd0310 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchOptions.java @@ -23,6 +23,9 @@ /** * Represents optional fields of the {@code $vectorSearch} pipeline stage of an aggregation pipeline. * + *

This includes options for searching against nested (embedded) embeddings and arrays of embeddings, + * via {@link #parentFilter(Bson)} and {@link #nestedOptions(VectorSearchNestedOptions)}.

+ * * @see Aggregates#vectorSearch(FieldSearchPath, Iterable, String, long, VectorSearchOptions) * @mongodb.atlas.manual atlas-vector-search/vector-search-stage/ $vectorSearch * @mongodb.server.release 6.0.11 @@ -41,6 +44,41 @@ public interface VectorSearchOptions extends Bson { */ VectorSearchOptions filter(Bson filter); + /** + * Creates a new {@link VectorSearchOptions} with the parent filter specified. + * + *

Applies only when searching against a nested (embedded) field: this filter is applied to the + * parent (root) documents, while {@link #filter(Bson)} is applied to the leaf (embedded) documents. + * For a non-nested search, use {@link #filter(Bson)} instead.

+ *

Unlike some other MongoDB drivers, the Java driver does not automatically re-route a top-level + * {@link #filter(Bson)} to {@code parentFilter} based on the path: you must call + * {@link #parentFilter(Bson)} explicitly to specify a parent (root-level) filter for a nested search.

+ * + * @param parentFilter A filter applied to the parent documents of a nested {@code $vectorSearch}. + * One may use {@link Filters} to create this filter, though not all filters may be supported. + * See the MongoDB documentation for the list of supported filters. + * @return A new {@link VectorSearchOptions}. + * @mongodb.atlas.manual atlas-vector-search/vector-search-stage/ $vectorSearch + * @mongodb.server.release 8.3 + * @since 5.10 + */ + VectorSearchOptions parentFilter(Bson parentFilter); + + /** + * Creates a new {@link VectorSearchOptions} with the {@code nestedOptions} specified. + * + *

Applies only when searching against arrays of embeddings within nested (embedded) documents; + * for example it controls how the scores of the individual matching embeddings within a document are + * aggregated (see {@link VectorSearchNestedOptions#scoreMode(VectorSearchScoreMode)}).

+ * + * @param nestedOptions The options for a nested (embedded) {@code $vectorSearch}. + * @return A new {@link VectorSearchOptions}. + * @mongodb.atlas.manual atlas-vector-search/vector-search-stage/ $vectorSearch + * @mongodb.server.release 8.3 + * @since 5.10 + */ + VectorSearchOptions nestedOptions(VectorSearchNestedOptions nestedOptions); + /** * Creates a new {@link VectorSearchOptions} that instructs to return only stored source fields. * diff --git a/driver-core/src/main/com/mongodb/client/model/search/VectorSearchScoreMode.java b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchScoreMode.java new file mode 100644 index 00000000000..917573024e2 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/VectorSearchScoreMode.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * The score aggregation mode for a {@code $vectorSearch} against arrays of embeddings in nested (embedded) documents. + * + *

If {@code scoreMode} is not specified, the server default is {@link #MAX} ({@code "max"}).

+ * + * @see VectorSearchNestedOptions#scoreMode(VectorSearchScoreMode) + * @mongodb.atlas.manual atlas-vector-search/vector-search-stage/ $vectorSearch + * @mongodb.server.release 8.3 + * @since 5.10 + */ +public enum VectorSearchScoreMode { + /** + * Use the average of the scores of the matching embeddings within a document. + */ + AVG("avg"), + + /** + * Use the maximum of the scores of the matching embeddings within a document. + */ + MAX("max"); + + private final String value; + + VectorSearchScoreMode(final String value) { + this.value = value; + } + + /** + * Returns the value used by the server for this score mode. + * + * @return the server value ({@code "avg"} or {@code "max"}). + * @since 5.10 + */ + public String getValue() { + return value; + } +} diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchNestedOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchNestedOptionsTest.java new file mode 100644 index 00000000000..6b1918ff014 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchNestedOptionsTest.java @@ -0,0 +1,108 @@ +/* + * 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 org.bson.BsonDocument; +import org.bson.BsonString; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class VectorSearchNestedOptionsTest { + @Test + void empty() { + assertEquals( + new BsonDocument(), + VectorSearchNestedOptions.vectorSearchNestedOptions().toBsonDocument() + ); + } + + @Test + void scoreMode() { + assertEquals( + new BsonDocument("scoreMode", new BsonString("avg")), + VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.AVG) + .toBsonDocument() + ); + } + + @Test + void scoreModeMaximum() { + assertEquals( + new BsonDocument("scoreMode", new BsonString("max")), + VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.MAX) + .toBsonDocument() + ); + } + + @Test + void scoreModeLastWriteWins() { + assertEquals( + new BsonDocument("scoreMode", new BsonString("avg")), + VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.MAX) + .scoreMode(VectorSearchScoreMode.AVG) + .toBsonDocument() + ); + } + + @Test + void optionEquivalentToScoreMode() { + assertEquals( + VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.AVG) + .toBsonDocument(), + VectorSearchNestedOptions.vectorSearchNestedOptions() + .option("scoreMode", "avg") + .toBsonDocument() + ); + } + + @Test + void scoreModeNull() { + assertThrows(IllegalArgumentException.class, () -> + VectorSearchNestedOptions.vectorSearchNestedOptions().scoreMode(null)); + } + + @Test + void optionNullName() { + assertThrows(IllegalArgumentException.class, () -> + VectorSearchNestedOptions.vectorSearchNestedOptions().option(null, "value")); + } + + @Test + void optionNullValue() { + assertThrows(IllegalArgumentException.class, () -> + VectorSearchNestedOptions.vectorSearchNestedOptions().option("scoreMode", null)); + } + + @Test + void vectorSearchNestedOptionsIsUnmodifiable() { + String expected = VectorSearchNestedOptions.vectorSearchNestedOptions().toBsonDocument().toJson(); + VectorSearchNestedOptions.vectorSearchNestedOptions().option("name", "value"); + assertEquals(expected, VectorSearchNestedOptions.vectorSearchNestedOptions().toBsonDocument().toJson()); + } + + @Test + void vectorSearchNestedOptionsIsImmutable() { + String expected = VectorSearchNestedOptions.vectorSearchNestedOptions().toBsonDocument().toJson(); + VectorSearchNestedOptions.vectorSearchNestedOptions().toBsonDocument().append("name", new BsonString("value")); + assertEquals(expected, VectorSearchNestedOptions.vectorSearchNestedOptions().toBsonDocument().toJson()); + } +} diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java new file mode 100644 index 00000000000..13106662814 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java @@ -0,0 +1,81 @@ +/* + * 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.client.model.Filters; +import org.bson.BsonDocument; +import org.bson.BsonInt64; +import org.bson.BsonString; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class VectorSearchOptionsTest { + @Test + void parentFilter() { + assertEquals( + new BsonDocument() + .append("parentFilter", Filters.gt("year", 1900).toBsonDocument()) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .parentFilter(Filters.gt("year", 1900)) + .toBsonDocument() + ); + } + + @Test + void nestedOptions() { + assertEquals( + new BsonDocument() + .append("nestedOptions", new BsonDocument("scoreMode", new BsonString("avg"))) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .nestedOptions(VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.AVG)) + .toBsonDocument() + ); + } + + @Test + void filterParentFilterAndNestedOptions() { + assertEquals( + new BsonDocument() + .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) + .append("parentFilter", Filters.gt("year", 1900).toBsonDocument()) + .append("nestedOptions", new BsonDocument("scoreMode", new BsonString("avg"))) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .filter(Filters.lt("fieldName", 1)) + .parentFilter(Filters.gt("year", 1900)) + .nestedOptions(VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.AVG)) + .toBsonDocument() + ); + } + + @Test + void parentFilterNull() { + assertThrows(IllegalArgumentException.class, () -> + VectorSearchOptions.approximateVectorSearchOptions(1).parentFilter(null)); + } + + @Test + void nestedOptionsNull() { + assertThrows(IllegalArgumentException.class, () -> + VectorSearchOptions.approximateVectorSearchOptions(1).nestedOptions(null)); + } +} diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchNestedOptions.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchNestedOptions.scala new file mode 100644 index 00000000000..178818a8910 --- /dev/null +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchNestedOptions.scala @@ -0,0 +1,35 @@ +/* + * 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 org.mongodb.scala.model.search + +import com.mongodb.client.model.search.{ VectorSearchNestedOptions => JVectorSearchNestedOptions } + +/** + * Represents the optional `\$vectorSearch` `nestedOptions` sub-document, + * used when searching against arrays of embeddings within nested (embedded) documents. + * + * @see [[https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ \$vectorSearch]] + * @since 5.10 + */ +object VectorSearchNestedOptions { + + /** + * Returns `VectorSearchNestedOptions` that represents server defaults. + * + * @return `VectorSearchNestedOptions` that represents server defaults. + */ + def vectorSearchNestedOptions(): VectorSearchNestedOptions = JVectorSearchNestedOptions.vectorSearchNestedOptions() +} diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchScoreMode.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchScoreMode.scala new file mode 100644 index 00000000000..7731c973a88 --- /dev/null +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/VectorSearchScoreMode.scala @@ -0,0 +1,38 @@ +/* + * 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 org.mongodb.scala.model.search + +import com.mongodb.client.model.search.{ VectorSearchScoreMode => JVectorSearchScoreMode } + +/** + * The score aggregation mode for a `\$vectorSearch` against arrays of embeddings in nested (embedded) documents. + * + * @see [[https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ \$vectorSearch]] + * @since 5.10 + */ +object VectorSearchScoreMode { + + /** + * Use the average of the scores of the matching embeddings within a document. + */ + val AVG: JVectorSearchScoreMode = JVectorSearchScoreMode.AVG + + /** + * Use the maximum of the scores of the matching embeddings within a document. + */ + val MAX: JVectorSearchScoreMode = JVectorSearchScoreMode.MAX +} 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 01ddaffb29f..f6c321da5db 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 @@ -340,6 +340,24 @@ package object search { @Beta(Array(Reason.SERVER)) type ExactVectorSearchOptions = com.mongodb.client.model.search.ExactVectorSearchOptions + /** + * Represents the optional `nestedOptions` sub-document of the `\$vectorSearch` pipeline stage, + * used when searching against arrays of embeddings within nested (embedded) documents. + * + * @see [[https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ \$vectorSearch]] + * @since 5.10 + */ + @Sealed + type VectorSearchNestedOptions = com.mongodb.client.model.search.VectorSearchNestedOptions + + /** + * The score aggregation mode for a `\$vectorSearch` against arrays of embeddings in nested (embedded) documents. + * + * @see [[https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/ \$vectorSearch]] + * @since 5.10 + */ + type VectorSearchScoreMode = com.mongodb.client.model.search.VectorSearchScoreMode + /** * Highlighting options. * You may use the `\$meta: "searchHighlights"` expression, e.g., via [[Projections.metaSearchHighlights]], diff --git a/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala index 99fb5a5e783..2dcce0cfa75 100644 --- a/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -308,13 +308,15 @@ class ApiAliasAndCompanionSpec extends BaseSpec { it should "mirror all com.mongodb.client.model.search in org.mongdb.scala.model.search" in { val packageName = "com.mongodb.client.model.search" - val wrapped = new Reflections(packageName, new SubTypesScanner(false)) - .getSubTypesOf(classOf[Object]) - .asScala - .filter(_.getPackage.getName == packageName) - .filter(classFilter) - .map(_.getSimpleName) - .toSet + val wrapped = + (new Reflections(packageName, new SubTypesScanner(false)) + .getSubTypesOf(classOf[Object]) + .asScala ++ + new Reflections(packageName, new SubTypesScanner(false)).getSubTypesOf(classOf[Enum[_]]).asScala) + .filter(_.getPackage.getName == packageName) + .filter(classFilter) + .map(_.getSimpleName) + .toSet val scalaPackageName = "org.mongodb.scala.model.search" val localPackage = currentMirror.staticPackage(scalaPackageName).info.decls.map(_.name.toString).toSet val localObjects = new Reflections(scalaPackageName, new SubTypesScanner(false)) From a6b4bfeb50dde54cc2b15d47e81862042abc85c0 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 23 Jul 2026 16:43:49 +0100 Subject: [PATCH 2/7] Remove stale @Beta from Scala VectorSearchOptions aliases The Java VectorSearchOptions, ApproximateVectorSearchOptions and ExactVectorSearchOptions interfaces are stable (@Sealed, not @Beta), but their Scala type aliases still carried @Beta(Reason.SERVER) left over from when the feature was in beta. Drop the stale annotations so the Scala surface matches the Java classes. --- .../main/scala/org/mongodb/scala/model/search/package.scala | 3 --- 1 file changed, 3 deletions(-) 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 f6c321da5db..2c919c6f9b1 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 @@ -310,7 +310,6 @@ package object search { * @since 4.11 */ @Sealed - @Beta(Array(Reason.SERVER)) type VectorSearchOptions = com.mongodb.client.model.search.VectorSearchOptions /** @@ -323,7 +322,6 @@ package object search { * @since 5.2 */ @Sealed - @Beta(Array(Reason.SERVER)) type ApproximateVectorSearchOptions = com.mongodb.client.model.search.ApproximateVectorSearchOptions /** @@ -337,7 +335,6 @@ package object search { * @since 5.2 */ @Sealed - @Beta(Array(Reason.SERVER)) type ExactVectorSearchOptions = com.mongodb.client.model.search.ExactVectorSearchOptions /** From 910415b98fad4774f574c4444b9ea4fa1850a24f Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 23 Jul 2026 16:43:50 +0100 Subject: [PATCH 3/7] Document Scala wrapper mirroring convention in driver-scala AGENTS.md Adding a public Java type/enum under a wrapped package requires a matching Scala wrapper (enforced by ApiAliasAndCompanionSpec): a type alias with stability annotations kept in sync with the Java class, a companion object re-exposing enum constants, and factory objects following each package's convention. --- driver-scala/AGENTS.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/driver-scala/AGENTS.md b/driver-scala/AGENTS.md index 3e22456ea26..0b339e42ab9 100644 --- a/driver-scala/AGENTS.md +++ b/driver-scala/AGENTS.md @@ -23,3 +23,27 @@ Scala async driver providing Observable-based API wrapping `driver-reactive-stre ``` See [README.md](./README.md) for directory layout details. + +## Notes + +- **Mirror new public Java types in Scala.** Adding a public type or enum under a + wrapped package (`com.mongodb`, `com.mongodb.client.model` and its subpackages, + `com.mongodb.connection`, `com.mongodb.client.result`, …) requires a matching wrapper + in the corresponding `org.mongodb.scala.*` package — this is enforced by + `ApiAliasAndCompanionSpec` (the build fails if a mirror is missing): + - Add a `type` alias in the package's `package.scala`, copying the Java class's + stability annotations (`@Sealed`, `@Beta(Reason.…)`). + - **Keep `@Beta` annotations in sync with their Java counterpart.** The Scala alias + (and any companion `object`) must carry `@Beta` if and only if the Java class does, + with the same `Reason` value(s). When a Java type is promoted to stable (its + `@Beta` removed) or its `Reason` changes, update the Scala side to match — a + stable Java class (no `@Beta`) must not carry a stale `@Beta` on its Scala alias. + - **Every wrapped public enum needs a companion `object`** re-exposing each constant + as a `val` (see `ReturnDocument.scala`, `CollationStrength.scala`); a `type` alias + alone does not bring the enum constants into term scope, so `MyEnum.CONSTANT` would + not resolve for Scala users. + - For a type with factory methods, add a companion `object`. Follow the surrounding + package's convention: `model` uses an inline `object X { def apply(): X = new … }` + in `package.scala` for constructor-based options, whereas `model/search` uses a + dedicated-file `object` exposing each factory under its Java name (e.g. + `vectorSearchNestedOptions()`), never `apply()`. From d70ab35ea9bfb429865f317b62cd0589eaa72b02 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 28 Jul 2026 09:11:46 +0100 Subject: [PATCH 4/7] Remove stale @Beta annotation in search options --- .../src/main/com/mongodb/client/model/search/SearchOptions.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOptions.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOptions.java index f5cd0261e8f..6fcbb18b181 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOptions.java @@ -54,7 +54,6 @@ public interface SearchOptions extends Bson { * @param option The counting option. * @return A new {@link SearchOptions}. */ - @Beta({Reason.CLIENT, Reason.SERVER}) SearchOptions count(SearchCount option); /** @@ -64,7 +63,6 @@ public interface SearchOptions extends Bson { * @return A new {@link SearchOptions}. * @mongodb.atlas.manual atlas-search/return-stored-source/ Return stored source fields */ - @Beta({Reason.CLIENT, Reason.SERVER}) SearchOptions returnStoredSource(boolean returnStoredSource); /** From 60190f5ec4de7174a16c71a6bd62ac32bf13b5f5 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 28 Jul 2026 11:26:06 +0100 Subject: [PATCH 5/7] Reuse a single Reflections instance in search mirror test Avoids scanning com.mongodb.client.model.search twice when collecting both Object and Enum subtypes. --- .../org/mongodb/scala/ApiAliasAndCompanionSpec.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala index 2dcce0cfa75..1a5b62094c6 100644 --- a/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala-2/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -308,11 +308,10 @@ class ApiAliasAndCompanionSpec extends BaseSpec { it should "mirror all com.mongodb.client.model.search in org.mongdb.scala.model.search" in { val packageName = "com.mongodb.client.model.search" + val reflections = new Reflections(packageName, new SubTypesScanner(false)) val wrapped = - (new Reflections(packageName, new SubTypesScanner(false)) - .getSubTypesOf(classOf[Object]) - .asScala ++ - new Reflections(packageName, new SubTypesScanner(false)).getSubTypesOf(classOf[Enum[_]]).asScala) + (reflections.getSubTypesOf(classOf[Object]).asScala ++ + reflections.getSubTypesOf(classOf[Enum[_]]).asScala) .filter(_.getPackage.getName == packageName) .filter(classFilter) .map(_.getSimpleName) From 9756cb0e940b27b2bacd2cb3aa0c35d4d64efded Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 29 Jul 2026 09:25:52 +0100 Subject: [PATCH 6/7] Test last-write-wins for parentFilter and nestedOptions --- .../model/search/VectorSearchOptionsTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java index 13106662814..e2a16a42200 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java @@ -67,6 +67,34 @@ void filterParentFilterAndNestedOptions() { ); } + @Test + void parentFilterLastWriteWins() { + assertEquals( + new BsonDocument() + .append("parentFilter", Filters.gt("year", 2000).toBsonDocument()) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .parentFilter(Filters.gt("year", 1900)) + .parentFilter(Filters.gt("year", 2000)) + .toBsonDocument() + ); + } + + @Test + void nestedOptionsLastWriteWins() { + assertEquals( + new BsonDocument() + .append("nestedOptions", new BsonDocument("scoreMode", new BsonString("max"))) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .nestedOptions(VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.AVG)) + .nestedOptions(VectorSearchNestedOptions.vectorSearchNestedOptions() + .scoreMode(VectorSearchScoreMode.MAX)) + .toBsonDocument() + ); + } + @Test void parentFilterNull() { assertThrows(IllegalArgumentException.class, () -> From 31784da9ca36ff971ade896bd0044e23b969b60d Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 29 Jul 2026 09:32:08 +0100 Subject: [PATCH 7/7] Merge BinaryVectorSearchOptionsTest into VectorSearchOptionsTest So the test class maps directly to the VectorSearchOptions interface it covers, matching the rest of the package. --- .../search/BinaryVectorSearchOptionsTest.java | 150 ------------------ .../model/search/VectorSearchOptionsTest.java | 124 +++++++++++++++ 2 files changed, 124 insertions(+), 150 deletions(-) delete mode 100644 driver-core/src/test/unit/com/mongodb/client/model/search/BinaryVectorSearchOptionsTest.java diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/BinaryVectorSearchOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/BinaryVectorSearchOptionsTest.java deleted file mode 100644 index 952974b8edd..00000000000 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/BinaryVectorSearchOptionsTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.client.model.Filters; -import org.bson.BsonBoolean; -import org.bson.BsonDocument; -import org.bson.BsonInt64; -import org.bson.BsonString; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -final class BinaryVectorSearchOptionsTest { - @Test - void approximateVectorSearchOptions() { - assertEquals( - new BsonDocument().append("numCandidates", new BsonInt64(1)), - VectorSearchOptions.approximateVectorSearchOptions(1) - .toBsonDocument() - ); - } - - @Test - void exactVectorSearchOptions() { - assertEquals( - new BsonDocument().append("exact", new BsonBoolean(true)), - VectorSearchOptions.exactVectorSearchOptions() - .toBsonDocument() - ); - } - - @Test - void option() { - assertEquals( - VectorSearchOptions.approximateVectorSearchOptions(1) - .filter(Filters.lt("fieldName", 1)) - .toBsonDocument(), - VectorSearchOptions.approximateVectorSearchOptions(1) - .option("filter", Filters.lt("fieldName", 1)) - .toBsonDocument()); - } - - @Test - void filterApproximate() { - assertEquals( - new BsonDocument() - .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) - .append("numCandidates", new BsonInt64(1)), - VectorSearchOptions.approximateVectorSearchOptions(1) - .filter(Filters.lt("fieldName", 1)) - .toBsonDocument() - ); - } - - @Test - void filterExact() { - assertEquals( - new BsonDocument() - .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) - .append("exact", new BsonBoolean(true)), - VectorSearchOptions.exactVectorSearchOptions() - .filter(Filters.lt("fieldName", 1)) - .toBsonDocument() - ); - } - - @Test - void optionsApproximate() { - assertEquals( - new BsonDocument() - .append("name", new BsonString("value")) - .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) - .append("numCandidates", new BsonInt64(1)), - VectorSearchOptions.approximateVectorSearchOptions(1) - .option("name", "value") - .filter(Filters.lt("fieldName", 0)) - .option("filter", Filters.lt("fieldName", 1)) - .option("numCandidates", new BsonInt64(1)) - .toBsonDocument() - ); - } - - @Test - void optionsExact() { - assertEquals( - new BsonDocument() - .append("name", new BsonString("value")) - .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) - .append("exact", new BsonBoolean(true)), - VectorSearchOptions.exactVectorSearchOptions() - .option("name", "value") - .filter(Filters.lt("fieldName", 0)) - .option("filter", Filters.lt("fieldName", 1)) - .option("exact", new BsonBoolean(true)) - .toBsonDocument() - ); - } - - @Test - void returnStoredSourceApproximate() { - assertEquals( - new BsonDocument() - .append("returnStoredSource", new BsonBoolean(true)) - .append("numCandidates", new BsonInt64(1)), - VectorSearchOptions.approximateVectorSearchOptions(1) - .returnStoredSource(true) - .toBsonDocument() - ); - } - - @Test - void returnStoredSourceExact() { - assertEquals( - new BsonDocument() - .append("returnStoredSource", new BsonBoolean(true)) - .append("exact", new BsonBoolean(true)), - VectorSearchOptions.exactVectorSearchOptions() - .returnStoredSource(true) - .toBsonDocument() - ); - } - - @Test - void approximateVectorSearchOptionsIsUnmodifiable() { - String expected = VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson(); - VectorSearchOptions.approximateVectorSearchOptions(1).option("name", "value"); - assertEquals(expected, VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson()); - } - - @Test - void approximateVectorSearchOptionsIsImmutable() { - String expected = VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson(); - VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().append("name", new BsonString("value")); - assertEquals(expected, VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson()); - } -} diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java index e2a16a42200..5b59cb74170 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/VectorSearchOptionsTest.java @@ -16,6 +16,7 @@ package com.mongodb.client.model.search; import com.mongodb.client.model.Filters; +import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonInt64; import org.bson.BsonString; @@ -25,6 +26,59 @@ import static org.junit.jupiter.api.Assertions.assertThrows; final class VectorSearchOptionsTest { + @Test + void approximateVectorSearchOptions() { + assertEquals( + new BsonDocument().append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .toBsonDocument() + ); + } + + @Test + void exactVectorSearchOptions() { + assertEquals( + new BsonDocument().append("exact", new BsonBoolean(true)), + VectorSearchOptions.exactVectorSearchOptions() + .toBsonDocument() + ); + } + + @Test + void option() { + assertEquals( + VectorSearchOptions.approximateVectorSearchOptions(1) + .filter(Filters.lt("fieldName", 1)) + .toBsonDocument(), + VectorSearchOptions.approximateVectorSearchOptions(1) + .option("filter", Filters.lt("fieldName", 1)) + .toBsonDocument()); + } + + @Test + void filterApproximate() { + assertEquals( + new BsonDocument() + .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .filter(Filters.lt("fieldName", 1)) + .toBsonDocument() + ); + } + + @Test + void filterExact() { + assertEquals( + new BsonDocument() + .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) + .append("exact", new BsonBoolean(true)), + VectorSearchOptions.exactVectorSearchOptions() + .filter(Filters.lt("fieldName", 1)) + .toBsonDocument() + ); + } + @Test void parentFilter() { assertEquals( @@ -67,6 +121,62 @@ void filterParentFilterAndNestedOptions() { ); } + @Test + void optionsApproximate() { + assertEquals( + new BsonDocument() + .append("name", new BsonString("value")) + .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .option("name", "value") + .filter(Filters.lt("fieldName", 0)) + .option("filter", Filters.lt("fieldName", 1)) + .option("numCandidates", new BsonInt64(1)) + .toBsonDocument() + ); + } + + @Test + void optionsExact() { + assertEquals( + new BsonDocument() + .append("name", new BsonString("value")) + .append("filter", Filters.lt("fieldName", 1).toBsonDocument()) + .append("exact", new BsonBoolean(true)), + VectorSearchOptions.exactVectorSearchOptions() + .option("name", "value") + .filter(Filters.lt("fieldName", 0)) + .option("filter", Filters.lt("fieldName", 1)) + .option("exact", new BsonBoolean(true)) + .toBsonDocument() + ); + } + + @Test + void returnStoredSourceApproximate() { + assertEquals( + new BsonDocument() + .append("returnStoredSource", new BsonBoolean(true)) + .append("numCandidates", new BsonInt64(1)), + VectorSearchOptions.approximateVectorSearchOptions(1) + .returnStoredSource(true) + .toBsonDocument() + ); + } + + @Test + void returnStoredSourceExact() { + assertEquals( + new BsonDocument() + .append("returnStoredSource", new BsonBoolean(true)) + .append("exact", new BsonBoolean(true)), + VectorSearchOptions.exactVectorSearchOptions() + .returnStoredSource(true) + .toBsonDocument() + ); + } + @Test void parentFilterLastWriteWins() { assertEquals( @@ -106,4 +216,18 @@ void nestedOptionsNull() { assertThrows(IllegalArgumentException.class, () -> VectorSearchOptions.approximateVectorSearchOptions(1).nestedOptions(null)); } + + @Test + void approximateVectorSearchOptionsIsUnmodifiable() { + String expected = VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson(); + VectorSearchOptions.approximateVectorSearchOptions(1).option("name", "value"); + assertEquals(expected, VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson()); + } + + @Test + void approximateVectorSearchOptionsIsImmutable() { + String expected = VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson(); + VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().append("name", new BsonString("value")); + assertEquals(expected, VectorSearchOptions.approximateVectorSearchOptions(1).toBsonDocument().toJson()); + } }