diff --git a/evergreen/evergreen.yml b/evergreen/evergreen.yml index 617e84b5b30..15a17b5fea2 100644 --- a/evergreen/evergreen.yml +++ b/evergreen/evergreen.yml @@ -2099,7 +2099,7 @@ task_groups: - "AWS_SESSION_TOKEN" env: CLUSTER_PREFIX: dbx-csharp-search-index - MONGODB_VERSION: "7.0" + MONGODB_VERSION: "8.0" args: - ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh - command: expansions.update diff --git a/src/MongoDB.Driver/CreateSearchIndexModel.cs b/src/MongoDB.Driver/CreateSearchIndexModel.cs index bb5a2498a4f..b0839e68bb5 100644 --- a/src/MongoDB.Driver/CreateSearchIndexModel.cs +++ b/src/MongoDB.Driver/CreateSearchIndexModel.cs @@ -13,45 +13,79 @@ * limitations under the License. */ +using System; using MongoDB.Bson; namespace MongoDB.Driver { /// - /// Model for creating a search index. + /// Defines a search index model using a definition. Consider using + /// to build vector indexes without specifying the BSON directly. /// - public sealed class CreateSearchIndexModel + public class CreateSearchIndexModel { + private readonly BsonDocument _definition; + private readonly SearchIndexType? _type; + private readonly string _name; + /// Gets the index name. /// The index name. - public string Name { get; } + public string Name => _name; /// Gets the index type. /// The index type. - public SearchIndexType? Type { get; } + public SearchIndexType? Type => _type; - /// Gets the index definition. + /// + /// Gets the index definition, if one was passed to a constructor of this class, otherwise throws. + /// /// The definition. - public BsonDocument Definition { get; } + public BsonDocument Definition + => _definition ?? throw new NotSupportedException( + "This method should not be called on this subtype. Instead, call 'Render' to create a BSON document for the index model."); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class, passing the index + /// model as a . /// - /// The name. - /// The definition. - public CreateSearchIndexModel(string name, BsonDocument definition) : this(name, null, definition) { } + /// + /// Consider using to build vector indexes without specifying + /// the BSON directly. + /// + /// The index name. + /// The index definition. + public CreateSearchIndexModel(string name, BsonDocument definition) + : this(name, null, definition) + { + } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class, passing the index + /// model as a . /// - /// The name. - /// The type. - /// The definition. + /// + /// Consider using to build vector indexes without specifying + /// the BSON directly. + /// + /// The index name. + /// The index type. + /// The index definition. public CreateSearchIndexModel(string name, SearchIndexType? type, BsonDocument definition) { - Name = name; - Type = type; - Definition = definition; + _name = name; + _type = type; + _definition = definition; + } + + /// + /// Initializes a new instance of the class. + /// + /// The index name. + /// The index type. + protected CreateSearchIndexModel(string name, SearchIndexType? type) + { + _name = name; + _type = type; } } } diff --git a/src/MongoDB.Driver/CreateVectorSearchIndexModel.cs b/src/MongoDB.Driver/CreateVectorSearchIndexModel.cs new file mode 100644 index 00000000000..0d82df576a4 --- /dev/null +++ b/src/MongoDB.Driver/CreateVectorSearchIndexModel.cs @@ -0,0 +1,165 @@ +/* Copyright 2010-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. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MongoDB.Bson; + +namespace MongoDB.Driver; + +/// +/// Defines a vector index model using strongly-typed C# APIs. +/// +public sealed class CreateVectorSearchIndexModel : CreateSearchIndexModel +{ + /// + /// The field containing the vectors to index. + /// + public FieldDefinition Field { get; } + + /// + /// The to use to search for top K-nearest neighbors. + /// + public VectorSimilarity Similarity { get; } + + /// + /// Number of vector dimensions that vector search enforces at index-time and query-time. + /// + public int Dimensions { get; } + + /// + /// Fields that may be used as filters in the vector query. + /// + public IReadOnlyList> FilterFields { get; } + + /// + /// Type of automatic vector quantization for your vectors. + /// + public VectorQuantization? Quantization { get; init; } + + /// + /// Maximum number of edges (or connections) that a node can have in the Hierarchical Navigable Small Worlds graph. + /// + public int? HnswMaxEdges { get; init; } + + /// + /// Analogous to numCandidates at query-time, this parameter controls the maximum number of nodes to evaluate to find the closest neighbors to connect to a new node. + /// + public int? HnswNumEdgeCandidates { get; init; } + + /// + /// Initializes a new instance of the class, passing the + /// required options for and the number of vector dimensions to the constructor. + /// + /// The index name. + /// The field containing the vectors to index. + /// The to use to search for top K-nearest neighbors. + /// Number of vector dimensions that vector search enforces at index-time and query-time. + /// Fields that may be used as filters in the vector query. + public CreateVectorSearchIndexModel( + FieldDefinition field, + string name, + VectorSimilarity similarity, + int dimensions, + params FieldDefinition[] filterFields) + : base(name, SearchIndexType.VectorSearch) + { + Field = field; + Similarity = similarity; + Dimensions = dimensions; + FilterFields = filterFields?.ToList() ?? []; + } + + /// + /// Initializes a new instance of the class, passing the + /// required options for and the number of vector dimensions to the constructor. + /// + /// The index name. + /// An expression pointing to the field containing the vectors to index. + /// The to use to search for top K-nearest neighbors. + /// Number of vector dimensions that vector search enforces at index-time and query-time. + /// Expressions pointing to fields that may be used as filters in the vector query. + public CreateVectorSearchIndexModel( + Expression> field, + string name, + VectorSimilarity similarity, + int dimensions, + params Expression>[] filterFields) + : this( + new ExpressionFieldDefinition(field), + name, + similarity, + dimensions, + filterFields? + .Select(f => (FieldDefinition)new ExpressionFieldDefinition(f)) + .ToArray()) + { + } + + /// + /// Renders the index model to a . + /// + /// The render arguments. + /// A . + public BsonDocument Render(RenderArgs renderArgs) + { + var similarityValue = Similarity == VectorSimilarity.DotProduct + ? "dotProduct" // Because neither "DotProduct" or "dotproduct" are allowed. + : Similarity.ToString().ToLowerInvariant(); + + var vectorField = new BsonDocument + { + { "type", BsonString.Create("vector") }, + { "path", Field.Render(renderArgs).FieldName }, + { "numDimensions", BsonInt32.Create(Dimensions) }, + { "similarity", BsonString.Create(similarityValue) }, + }; + + if (Quantization.HasValue) + { + vectorField.Add("quantization", BsonString.Create(Quantization.ToString()?.ToLower())); + } + + if (HnswMaxEdges != null || HnswNumEdgeCandidates != null) + { + var hnswDocument = new BsonDocument + { + { "maxEdges", BsonInt32.Create(HnswMaxEdges ?? 16) }, + { "numEdgeCandidates", BsonInt32.Create(HnswNumEdgeCandidates ?? 100) } + }; + vectorField.Add("hnswOptions", hnswDocument); + } + + var fieldDocuments = new List { vectorField }; + + if (FilterFields != null) + { + foreach (var filterPath in FilterFields) + { + var fieldDocument = new BsonDocument + { + { "type", BsonString.Create("filter") }, + { "path", BsonString.Create(filterPath.Render(renderArgs).FieldName) } + }; + + fieldDocuments.Add(fieldDocument); + } + } + + return new BsonDocument { { "fields", new BsonArray(fieldDocuments) } }; + } +} diff --git a/src/MongoDB.Driver/MongoCollectionImpl.cs b/src/MongoDB.Driver/MongoCollectionImpl.cs index 20b11c8c942..a083a40c453 100644 --- a/src/MongoDB.Driver/MongoCollectionImpl.cs +++ b/src/MongoDB.Driver/MongoCollectionImpl.cs @@ -1741,10 +1741,22 @@ private PipelineDefinition CreateListIndexesStage(strin return new BsonDocumentStagePipelineDefinition(new[] { stage }); } - private CreateSearchIndexesOperation CreateCreateIndexesOperation(IEnumerable models) => - new(_collection._collectionNamespace, - models.Select(m => new CreateSearchIndexRequest(m.Name, m.Type, m.Definition)), + private CreateSearchIndexesOperation CreateCreateIndexesOperation( + IEnumerable models) + { + var renderArgs = _collection.GetRenderArgs(); + + return new CreateSearchIndexesOperation( + _collection._collectionNamespace, + models.Select(model + => new CreateSearchIndexRequest( + model.Name, + model.Type, + model is CreateVectorSearchIndexModel createVectorSearchIndexModel + ? createVectorSearchIndexModel.Render(renderArgs) + : model.Definition)), _collection._messageEncoderSettings); + } private string[] GetIndexNames(BsonDocument createSearchIndexesResponse) => createSearchIndexesResponse["indexesCreated"] diff --git a/src/MongoDB.Driver/VectorQuantization.cs b/src/MongoDB.Driver/VectorQuantization.cs new file mode 100644 index 00000000000..0b0fa37297c --- /dev/null +++ b/src/MongoDB.Driver/VectorQuantization.cs @@ -0,0 +1,42 @@ +/* Copyright 2010-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. + */ + +namespace MongoDB.Driver; + +/// +/// Type of automatic vector quantization for your vectors. Use this setting only if your embeddings are float +/// or double vectors. See +/// Vector Quantization for more information. +/// +public enum VectorQuantization +{ + /// + /// Indicates no automatic quantization for the vector embeddings. Use this setting if you have pre-quantized + /// vectors for ingestion. If omitted, this is the default value. + /// + None, + + /// + /// Indicates scalar quantization, which transforms values to 1 byte integers. + /// + Scalar, + + /// + /// Indicates binary quantization, which transforms values to a single bit. + /// To use this value, numDimensions must be a multiple of 8. + /// If precision is critical, select or instead of . + /// + Binary, +} diff --git a/src/MongoDB.Driver/VectorSimilarity.cs b/src/MongoDB.Driver/VectorSimilarity.cs new file mode 100644 index 00000000000..fe000e400ec --- /dev/null +++ b/src/MongoDB.Driver/VectorSimilarity.cs @@ -0,0 +1,39 @@ +/* Copyright 2010-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. + */ + +namespace MongoDB.Driver; + +/// +/// Vector similarity function to use to search for top K-nearest neighbors. +/// See How to Index Fields for +/// Vector Search for more information. +/// +public enum VectorSimilarity +{ + /// + /// Measures the distance between ends of vectors. + /// + Euclidean, + + /// + /// Measures similarity based on the angle between vectors. + /// + Cosine, + + /// + /// Measures similarity like cosine, but takes into account the magnitude of the vector. + /// + DotProduct, +} diff --git a/tests/MongoDB.Driver.Tests/Search/AtlasSearchIndexManagmentTests.cs b/tests/MongoDB.Driver.Tests/Search/AtlasSearchIndexManagmentTests.cs index 798648fceec..c11bc735c55 100644 --- a/tests/MongoDB.Driver.Tests/Search/AtlasSearchIndexManagmentTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/AtlasSearchIndexManagmentTests.cs @@ -37,6 +37,7 @@ public class AtlasSearchIndexManagementTests : LoggableTestClass private readonly IMongoDatabase _database; private readonly IMongoClient _mongoClient; private readonly BsonDocument _indexDefinition = BsonDocument.Parse("{ mappings: { dynamic: false } }"); + private readonly BsonDocument _indexDefinitionWithFields = BsonDocument.Parse("{ mappings: { dynamic: false, fields: { } } }"); private readonly BsonDocument _vectorIndexDefinition = BsonDocument.Parse("{ fields: [ { type: 'vector', path: 'plot_embedding', numDimensions: 1536, similarity: 'euclidean' } ] }"); public AtlasSearchIndexManagementTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) @@ -68,10 +69,16 @@ public Task Case1_driver_should_successfully_create_and_list_search_indexes( [Theory(Timeout = Timeout)] [ParameterAttributeData] public async Task Case2_driver_should_successfully_create_multiple_indexes_in_batch( - [Values(false, true)] bool async) + [Values(false, true)] bool async, + [Values(false, true)] bool includeFields) { - var indexDefinition1 = new CreateSearchIndexModel(async ? "test-search-index-1-async" : "test-search-index-1", _indexDefinition); - var indexDefinition2 = new CreateSearchIndexModel(async ? "test-search-index-2-async" : "test-search-index-2", _indexDefinition); + var indexDefinition1 = new CreateSearchIndexModel( + async ? "test-search-index-1-async" : "test-search-index-1", + includeFields ? _indexDefinitionWithFields : _indexDefinition); + + var indexDefinition2 = new CreateSearchIndexModel( + async ? "test-search-index-2-async" : "test-search-index-2", + includeFields ? _indexDefinitionWithFields : _indexDefinition); var indexNamesActual = async ? await _collection.SearchIndexes.CreateManyAsync(new[] { indexDefinition1, indexDefinition2 }) @@ -81,8 +88,8 @@ public async Task Case2_driver_should_successfully_create_multiple_indexes_in_ba var indexes = await GetIndexes(async, indexDefinition1.Name, indexDefinition2.Name); - indexes[0]["latestDefinition"].AsBsonDocument.Should().Be(_indexDefinition); - indexes[1]["latestDefinition"].AsBsonDocument.Should().Be(_indexDefinition); + indexes[0]["latestDefinition"].AsBsonDocument.Should().Be(_indexDefinitionWithFields); + indexes[1]["latestDefinition"].AsBsonDocument.Should().Be(_indexDefinitionWithFields); } [Theory(Timeout = Timeout)] @@ -130,7 +137,7 @@ public async Task Case4_driver_can_update_a_search_index( [Values(false, true)] bool async) { var indexName = async ? "test-search-index-async" : "test-search-index"; - var indexNewDefinition = BsonDocument.Parse("{ mappings: { dynamic: true }}"); + var indexNewDefinition = BsonDocument.Parse("{ mappings: { dynamic: true, fields: { } }}"); await CreateIndexAndValidate(indexName, _indexDefinition, async); if (async) @@ -166,7 +173,8 @@ public async Task Case5_dropSearchIndex_suppresses_namespace_not_found_errors( [Theory(Timeout = Timeout)] [ParameterAttributeData] public async Task Case6_driver_can_create_and_list_search_indexes_with_non_default_read_write_concern( - [Values(false, true)] bool async) + [Values(false, true)] bool async, + [Values(false, true)] bool includeFields) { var indexName = async ? "test-search-index-case6-async" : "test-search-index-case6"; @@ -175,13 +183,18 @@ public async Task Case6_driver_can_create_and_list_search_indexes_with_non_defau .WithWriteConcern(WriteConcern.WMajority); var indexNameCreated = async - ? await collection.SearchIndexes.CreateOneAsync(_indexDefinition, indexName) - : collection.SearchIndexes.CreateOne(_indexDefinition, indexName); + ? await collection.SearchIndexes.CreateOneAsync(includeFields + ? _indexDefinitionWithFields + : _indexDefinition, indexName) + : collection.SearchIndexes.CreateOne( + includeFields + ? _indexDefinitionWithFields + : _indexDefinition, indexName); indexNameCreated.Should().Be(indexName); var indexes = await GetIndexes(async, indexName); - indexes[0]["latestDefinition"].AsBsonDocument.Should().Be(_indexDefinition); + indexes[0]["latestDefinition"].AsBsonDocument.Should().Be(_indexDefinitionWithFields); } [Theory(Timeout = Timeout)] @@ -231,10 +244,182 @@ public async Task Case8_driver_requires_explicit_type_to_create_vector_search_in var indexName = async ? "test-search-index-case8-error-async" : "test-search-index-case8-error"; var exception = async - ? await Record.ExceptionAsync(() => _collection.SearchIndexes.CreateOneAsync(_vectorIndexDefinition, indexName)) - : Record.Exception(() => _collection.SearchIndexes.CreateOne(_vectorIndexDefinition, indexName)); + ? await Record.ExceptionAsync(() => _collection.SearchIndexes.CreateOneAsync( + new CreateSearchIndexModel(indexName, _vectorIndexDefinition).Definition, indexName)) + : Record.Exception(() => _collection.SearchIndexes.CreateOne( + new CreateSearchIndexModel(indexName, _vectorIndexDefinition).Definition, indexName)); + + exception.Message.Should().Contain("Command createSearchIndexes failed: \"userCommand.indexes[0].mappings\" is required."); + } + + [Theory(Timeout = Timeout)] + [ParameterAttributeData] + public async Task Can_create_Atlas_vector_index_for_all_options_using_typed_API( + [Values(false, true)] bool async) + { + var indexName = async ? "test-index-vector-optional-async" : "test-index-vector-optional"; + + var indexModel = new CreateVectorSearchIndexModel( + e => e.Floats, indexName, VectorSimilarity.Cosine, dimensions: 2) + { + HnswMaxEdges = 18, HnswNumEdgeCandidates = 102, Quantization = VectorQuantization.Scalar + }; + + var collection = _database.GetCollection(_collection.CollectionNamespace.CollectionName); + var createdName = async + ? await collection.SearchIndexes.CreateOneAsync(indexModel) + : collection.SearchIndexes.CreateOne(indexModel); + + createdName.Should().Be(indexName); + + var index = (await GetIndexes(async, indexName))[0]; + index["type"].AsString.Should().Be("vectorSearch"); + + var fields = index["latestDefinition"].AsBsonDocument["fields"].AsBsonArray; + fields.Count.Should().Be(1); + + var indexField = fields[0].AsBsonDocument; + indexField["type"].AsString.Should().Be("vector"); + indexField["path"].AsString.Should().Be("Floats"); + indexField["numDimensions"].AsInt32.Should().Be(2); + indexField["similarity"].AsString.Should().Be("cosine"); + indexField["quantization"].AsString.Should().Be("scalar"); + indexField["hnswOptions"].AsBsonDocument["maxEdges"].AsInt32.Should().Be(18); + indexField["hnswOptions"].AsBsonDocument["numEdgeCandidates"].AsInt32.Should().Be(102); + } + + [Theory(Timeout = Timeout)] + [ParameterAttributeData] + public async Task Can_create_Atlas_vector_index_for_required_only_options_using_typed_API( + [Values(false, true)] bool async) + { + var indexName = async ? "test-index-vector-required-async" : "test-index-vector-required"; + + var indexModel = new CreateVectorSearchIndexModel("vectors", indexName, VectorSimilarity.Euclidean, dimensions: 4); + + var collection = _database.GetCollection(_collection.CollectionNamespace.CollectionName); + var createdName = async + ? await collection.SearchIndexes.CreateOneAsync(indexModel) + : collection.SearchIndexes.CreateOne(indexModel); + + createdName.Should().Be(indexName); + + var index = (await GetIndexes(async, indexName))[0]; + index["type"].AsString.Should().Be("vectorSearch"); + + var fields = index["latestDefinition"].AsBsonDocument["fields"].AsBsonArray; + fields.Count.Should().Be(1); + + var indexField = fields[0].AsBsonDocument; + indexField["type"].AsString.Should().Be("vector"); + indexField["path"].AsString.Should().Be("vectors"); + indexField["numDimensions"].AsInt32.Should().Be(4); + indexField["similarity"].AsString.Should().Be("euclidean"); + + indexField.Contains("quantization").Should().Be(false); + indexField.Contains("hnswOptions").Should().Be(false); + } - exception.Message.Should().Contain("Attribute mappings missing"); + [Theory(Timeout = Timeout)] + [ParameterAttributeData] + public async Task Can_create_Atlas_vector_index_for_all_options_using_typed_API_with_filters( + [Values(false, true)] bool async) + { + var indexName = async ? "test-index-vector-typed-filters-async" : "test-index-typed-filters"; + + var indexModel = new CreateVectorSearchIndexModel( + e => e.Floats, + indexName, + VectorSimilarity.Cosine, + dimensions: 2, + e => e.Filter1, e => e.Filter2, e => e.Filter3) + { + HnswMaxEdges = 18, + HnswNumEdgeCandidates = 102, + Quantization = VectorQuantization.Scalar, + }; + + var collection = _database.GetCollection(_collection.CollectionNamespace.CollectionName); + var createdName = async + ? await collection.SearchIndexes.CreateOneAsync(indexModel) + : collection.SearchIndexes.CreateOne(indexModel); + + createdName.Should().Be(indexName); + + var index = (await GetIndexes(async, indexName))[0]; + index["type"].AsString.Should().Be("vectorSearch"); + + var fields = index["latestDefinition"].AsBsonDocument["fields"].AsBsonArray; + fields.Count.Should().Be(4); + + var indexField = fields[0].AsBsonDocument; + indexField["type"].AsString.Should().Be("vector"); + indexField["path"].AsString.Should().Be("Floats"); + indexField["numDimensions"].AsInt32.Should().Be(2); + indexField["similarity"].AsString.Should().Be("cosine"); + indexField["quantization"].AsString.Should().Be("scalar"); + indexField["hnswOptions"].AsBsonDocument["maxEdges"].AsInt32.Should().Be(18); + indexField["hnswOptions"].AsBsonDocument["numEdgeCandidates"].AsInt32.Should().Be(102); + + for (var i = 1; i <= 3; i++) + { + var filterField = fields[i].AsBsonDocument; + filterField["type"].AsString.Should().Be("filter"); + filterField["path"].AsString.Should().Be($"Filter{i}"); + } + } + + [Theory(Timeout = Timeout)] + [ParameterAttributeData] + public async Task Can_create_Atlas_vector_index_for_required_only_options_using_typed_API_with_filters( + [Values(false, true)] bool async) + { + var indexName = async ? "test-index-untyped-filters-async" : "test-index-untyped-filters"; + + var indexModel = new CreateVectorSearchIndexModel( + "vectors", + indexName, + VectorSimilarity.Euclidean, + dimensions: 4, + "f1", "f2", "f3"); + + var collection = _database.GetCollection(_collection.CollectionNamespace.CollectionName); + var createdName = async + ? await collection.SearchIndexes.CreateOneAsync(indexModel) + : collection.SearchIndexes.CreateOne(indexModel); + + createdName.Should().Be(indexName); + + var index = (await GetIndexes(async, indexName))[0]; + index["type"].AsString.Should().Be("vectorSearch"); + + var fields = index["latestDefinition"].AsBsonDocument["fields"].AsBsonArray; + fields.Count.Should().Be(4); + + var indexField = fields[0].AsBsonDocument; + indexField["type"].AsString.Should().Be("vector"); + indexField["path"].AsString.Should().Be("vectors"); + indexField["numDimensions"].AsInt32.Should().Be(4); + indexField["similarity"].AsString.Should().Be("euclidean"); + + indexField.Contains("quantization").Should().Be(false); + indexField.Contains("hnswOptions").Should().Be(false); + + for (var i = 1; i <= 3; i++) + { + var filterField = fields[i].AsBsonDocument; + filterField["type"].AsString.Should().Be("filter"); + filterField["path"].AsString.Should().Be($"f{i}"); + } + } + + private class EntityWithVector + { + public ObjectId Id { get; set; } + public float[] Floats { get; set; } + public bool Filter1 { get; set; } + public string Filter2 { get; set; } + public int Filter3 { get; set; } } private async Task CreateIndexAndValidate(string indexName, BsonDocument indexDefinition, bool async)