Skip to content

Commit 79a8b6a

Browse files
committed
CSHARP-5717: Typed builders for vector indexes
Replaces #1769
1 parent 33a1986 commit 79a8b6a

File tree

9 files changed

+543
-41
lines changed

9 files changed

+543
-41
lines changed

evergreen/evergreen.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2099,7 +2099,7 @@ task_groups:
20992099
- "AWS_SESSION_TOKEN"
21002100
env:
21012101
CLUSTER_PREFIX: dbx-csharp-search-index
2102-
MONGODB_VERSION: "7.0"
2102+
MONGODB_VERSION: "8.0"
21032103
args:
21042104
- ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh
21052105
- command: expansions.update

src/MongoDB.Driver/CreateSearchIndexModel.cs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,48 @@
1818
namespace MongoDB.Driver
1919
{
2020
/// <summary>
21-
/// Model for creating a search index.
21+
/// Defines a vector search index model using a <see cref="BsonDocument"/> definition. Consider using
22+
/// <see cref="CreateVectorIndexModel{TDocument}"/> to build Atlas indexes without specifying the BSON directly.
2223
/// </summary>
23-
public sealed class CreateSearchIndexModel
24+
public sealed class CreateSearchIndexModel : CreateSearchIndexModelBase
2425
{
25-
/// <summary>Gets the index name.</summary>
26-
/// <value>The index name.</value>
27-
public string Name { get; }
28-
2926
/// <summary>Gets the index type.</summary>
3027
/// <value>The index type.</value>
31-
public SearchIndexType? Type { get; }
28+
public override SearchIndexType? Type { get; }
3229

3330
/// <summary>Gets the index definition.</summary>
3431
/// <value>The definition.</value>
3532
public BsonDocument Definition { get; }
3633

3734
/// <summary>
38-
/// Initializes a new instance of the <see cref="CreateSearchIndexModel"/> class.
35+
/// Initializes a new instance of the <see cref="CreateSearchIndexModel"/> class, passing the index
36+
/// model as a <see cref="BsonDocument"/>.
3937
/// </summary>
40-
/// <param name="name">The name.</param>
41-
/// <param name="definition">The definition.</param>
42-
public CreateSearchIndexModel(string name, BsonDocument definition) : this(name, null, definition) { }
38+
/// <remarks>
39+
/// Consider using <see cref="CreateVectorIndexModel{TDocument}"/> to build Atlas indexes without specifying the
40+
/// BSON directly.
41+
/// </remarks>
42+
/// <param name="name">The index name.</param>
43+
/// <param name="definition">The index definition.</param>
44+
public CreateSearchIndexModel(string name, BsonDocument definition)
45+
: this(name, null, definition)
46+
{
47+
}
4348

4449
/// <summary>
45-
/// Initializes a new instance of the <see cref="CreateSearchIndexModel"/> class.
50+
/// Initializes a new instance of the <see cref="CreateSearchIndexModel"/> class, passing the index
51+
/// model as a <see cref="BsonDocument"/>.
4652
/// </summary>
47-
/// <param name="name">The name.</param>
48-
/// <param name="type">The type.</param>
49-
/// <param name="definition">The definition.</param>
53+
/// <remarks>
54+
/// Consider using <see cref="CreateVectorIndexModel{TDocument}"/> to build indexes without specifying the
55+
/// BSON directly.
56+
/// </remarks>
57+
/// <param name="name">The index name.</param>
58+
/// <param name="type">The index type.</param>
59+
/// <param name="definition">The index definition.</param>
5060
public CreateSearchIndexModel(string name, SearchIndexType? type, BsonDocument definition)
61+
: base(name)
5162
{
52-
Name = name;
5363
Type = type;
5464
Definition = definition;
5565
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
namespace MongoDB.Driver;
17+
18+
/// <summary>
19+
/// Abstract base class for search and vector index definitions. Concrete implementations are
20+
/// <see cref="CreateSearchIndexModel"/> and <see cref="CreateVectorIndexModel{TDocument}"/>
21+
/// </summary>
22+
public abstract class CreateSearchIndexModelBase
23+
{
24+
/// <summary>Gets the index name.</summary>
25+
/// <value>The index name.</value>
26+
public virtual string Name { get; }
27+
28+
/// <summary>Gets the index type.</summary>
29+
/// <value>The index type.</value>
30+
public abstract SearchIndexType? Type { get; }
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="CreateSearchIndexModelBase"/> class.
34+
/// </summary>
35+
/// <param name="name">The index name.</param>
36+
protected CreateSearchIndexModelBase(string name)
37+
{
38+
Name = name;
39+
}
40+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using System.Linq.Expressions;
20+
using MongoDB.Bson;
21+
22+
namespace MongoDB.Driver;
23+
24+
/// <summary>
25+
/// Defines a vector index model using strongly-typed C# APIs.
26+
/// </summary>
27+
public sealed class CreateVectorIndexModel<TDocument> : CreateSearchIndexModelBase
28+
{
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="CreateVectorIndexModel{TDocument}"/> class, passing the
31+
/// required options for <see cref="VectorSimilarity"/> and the number of vector dimensions to the constructor.
32+
/// </summary>
33+
/// <param name="name">The index name.</param>
34+
/// <param name="field">The field containing the vectors to index.</param>
35+
/// <param name="similarity">The <see cref="VectorSimilarity"/> to use to search for top K-nearest neighbors.</param>
36+
/// <param name="dimensions">Number of vector dimensions that vector search enforces at index-time and query-time.</param>
37+
/// <param name="filterFields">Fields that may be used as filters in the vector query.</param>
38+
public CreateVectorIndexModel(
39+
FieldDefinition<TDocument> field,
40+
string name,
41+
VectorSimilarity similarity,
42+
int dimensions,
43+
params FieldDefinition<TDocument>[] filterFields)
44+
: base(name)
45+
{
46+
Field = field;
47+
Similarity = similarity;
48+
Dimensions = dimensions;
49+
FilterFields = filterFields?.ToList() ?? [];
50+
}
51+
52+
/// <summary>
53+
/// Initializes a new instance of the <see cref="CreateVectorIndexModel{TDocument}"/> class, passing the
54+
/// required options for <see cref="VectorSimilarity"/> and the number of vector dimensions to the constructor.
55+
/// </summary>
56+
/// <param name="name">The index name.</param>
57+
/// <param name="field">An expression pointing to the field containing the vectors to index.</param>
58+
/// <param name="similarity">The <see cref="VectorSimilarity"/> to use to search for top K-nearest neighbors.</param>
59+
/// <param name="dimensions">Number of vector dimensions that vector search enforces at index-time and query-time.</param>
60+
/// <param name="filterFields">Expressions pointing to fields that may be used as filters in the vector query.</param>
61+
public CreateVectorIndexModel(
62+
Expression<Func<TDocument, object>> field,
63+
string name,
64+
VectorSimilarity similarity,
65+
int dimensions,
66+
params Expression<Func<TDocument, object>>[] filterFields)
67+
: this(
68+
new ExpressionFieldDefinition<TDocument>(field),
69+
name,
70+
similarity,
71+
dimensions,
72+
filterFields?
73+
.Select(f => (FieldDefinition<TDocument>)new ExpressionFieldDefinition<TDocument>(f))
74+
.ToArray())
75+
{
76+
}
77+
78+
/// <summary>
79+
/// The field containing the vectors to index.
80+
/// </summary>
81+
public FieldDefinition<TDocument> Field { get; }
82+
83+
/// <summary>
84+
/// The <see cref="VectorSimilarity"/> to use to search for top K-nearest neighbors.
85+
/// </summary>
86+
public VectorSimilarity Similarity { get; }
87+
88+
/// <summary>
89+
/// Number of vector dimensions that vector search enforces at index-time and query-time.
90+
/// </summary>
91+
public int Dimensions { get; }
92+
93+
/// <summary>
94+
/// Fields that may be used as filters in the vector query.
95+
/// </summary>
96+
public IReadOnlyList<FieldDefinition<TDocument>> FilterFields { get; }
97+
98+
/// <summary>
99+
/// Type of automatic vector quantization for your vectors.
100+
/// </summary>
101+
public VectorQuantization? Quantization { get; init; }
102+
103+
/// <summary>
104+
/// Maximum number of edges (or connections) that a node can have in the Hierarchical Navigable Small Worlds graph.
105+
/// </summary>
106+
public int? HnswMaxEdges { get; init; }
107+
108+
/// <summary>
109+
/// 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.
110+
/// </summary>
111+
public int? HnswNumEdgeCandidates { get; init; }
112+
113+
/// <inheritdoc/>
114+
public override SearchIndexType? Type => SearchIndexType.VectorSearch;
115+
116+
/// <summary>
117+
/// Renders the index model to a <see cref="BsonDocument"/>.
118+
/// </summary>
119+
/// <param name="renderArgs">The render arguments.</param>
120+
/// <returns>A <see cref="BsonDocument" />.</returns>
121+
public BsonDocument Render(RenderArgs<TDocument> renderArgs)
122+
{
123+
var similarityValue = Similarity == VectorSimilarity.DotProduct
124+
? "dotProduct" // Because neither "DotProduct" or "dotproduct" are allowed.
125+
: Similarity.ToString().ToLowerInvariant();
126+
127+
var vectorField = new BsonDocument
128+
{
129+
{ "type", BsonString.Create("vector") },
130+
{ "path", Field.Render(renderArgs).FieldName },
131+
{ "numDimensions", BsonInt32.Create(Dimensions) },
132+
{ "similarity", BsonString.Create(similarityValue) },
133+
};
134+
135+
if (Quantization.HasValue)
136+
{
137+
vectorField.Add("quantization", BsonString.Create(Quantization.ToString()?.ToLower()));
138+
}
139+
140+
if (HnswMaxEdges != null || HnswNumEdgeCandidates != null)
141+
{
142+
var hnswDocument = new BsonDocument
143+
{
144+
{ "maxEdges", BsonInt32.Create(HnswMaxEdges ?? 16) },
145+
{ "numEdgeCandidates", BsonInt32.Create(HnswNumEdgeCandidates ?? 100) }
146+
};
147+
vectorField.Add("hnswOptions", hnswDocument);
148+
}
149+
150+
var fieldDocuments = new List<BsonDocument> { vectorField };
151+
152+
if (FilterFields != null)
153+
{
154+
foreach (var filterPath in FilterFields)
155+
{
156+
var fieldDocument = new BsonDocument
157+
{
158+
{ "type", BsonString.Create("filter") },
159+
{ "path", BsonString.Create(filterPath.Render(renderArgs).FieldName) }
160+
};
161+
162+
fieldDocuments.Add(fieldDocument);
163+
}
164+
}
165+
166+
return new BsonDocument { { "fields", BsonArray.Create(fieldDocuments) } };
167+
}
168+
}

src/MongoDB.Driver/MongoCollectionImpl.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,7 +1655,7 @@ public MongoSearchIndexManager(MongoCollectionImpl<TDocument> collection)
16551655
_collection = Ensure.IsNotNull(collection, nameof(collection));
16561656
}
16571657

1658-
public IEnumerable<string> CreateMany(IEnumerable<CreateSearchIndexModel> models, CancellationToken cancellationToken = default)
1658+
public IEnumerable<string> CreateMany(IEnumerable<CreateSearchIndexModelBase> models, CancellationToken cancellationToken = default)
16591659
{
16601660
using var session = _collection._operationExecutor.StartImplicitSession();
16611661
var operation = CreateCreateIndexesOperation(models);
@@ -1664,7 +1664,7 @@ public IEnumerable<string> CreateMany(IEnumerable<CreateSearchIndexModel> models
16641664
return GetIndexNames(result);
16651665
}
16661666

1667-
public async Task<IEnumerable<string>> CreateManyAsync(IEnumerable<CreateSearchIndexModel> models, CancellationToken cancellationToken = default)
1667+
public async Task<IEnumerable<string>> CreateManyAsync(IEnumerable<CreateSearchIndexModelBase> models, CancellationToken cancellationToken = default)
16681668
{
16691669
using var session = _collection._operationExecutor.StartImplicitSession();
16701670
var operation = CreateCreateIndexesOperation(models);
@@ -1676,7 +1676,7 @@ public async Task<IEnumerable<string>> CreateManyAsync(IEnumerable<CreateSearchI
16761676
public string CreateOne(BsonDocument definition, string name = null, CancellationToken cancellationToken = default) =>
16771677
CreateOne(new CreateSearchIndexModel(name, definition), cancellationToken);
16781678

1679-
public string CreateOne(CreateSearchIndexModel model, CancellationToken cancellationToken = default)
1679+
public string CreateOne(CreateSearchIndexModelBase model, CancellationToken cancellationToken = default)
16801680
{
16811681
var result = CreateMany(new[] { model }, cancellationToken);
16821682
return result.Single();
@@ -1685,7 +1685,7 @@ public string CreateOne(CreateSearchIndexModel model, CancellationToken cancella
16851685
public Task<string> CreateOneAsync(BsonDocument definition, string name = null, CancellationToken cancellationToken = default) =>
16861686
CreateOneAsync(new CreateSearchIndexModel(name, definition), cancellationToken);
16871687

1688-
public async Task<string> CreateOneAsync(CreateSearchIndexModel model, CancellationToken cancellationToken = default)
1688+
public async Task<string> CreateOneAsync(CreateSearchIndexModelBase model, CancellationToken cancellationToken = default)
16891689
{
16901690
var result = await CreateManyAsync(new[] { model }, cancellationToken).ConfigureAwait(false);
16911691
return result.Single();
@@ -1741,10 +1741,28 @@ private PipelineDefinition<TDocument, BsonDocument> CreateListIndexesStage(strin
17411741
return new BsonDocumentStagePipelineDefinition<TDocument, BsonDocument>(new[] { stage });
17421742
}
17431743

1744-
private CreateSearchIndexesOperation CreateCreateIndexesOperation(IEnumerable<CreateSearchIndexModel> models) =>
1745-
new(_collection._collectionNamespace,
1746-
models.Select(m => new CreateSearchIndexRequest(m.Name, m.Type, m.Definition)),
1744+
private CreateSearchIndexesOperation CreateCreateIndexesOperation(
1745+
IEnumerable<CreateSearchIndexModelBase> models)
1746+
{
1747+
var renderArgs = _collection.GetRenderArgs();
1748+
1749+
return new CreateSearchIndexesOperation(
1750+
_collection._collectionNamespace,
1751+
models.Select(model
1752+
=> new CreateSearchIndexRequest(
1753+
model.Name,
1754+
model.Type,
1755+
model switch
1756+
{
1757+
CreateSearchIndexModel createSearchIndexModel
1758+
=> createSearchIndexModel.Definition,
1759+
CreateVectorIndexModel<TDocument> createAtlasVectorIndexModel
1760+
=> createAtlasVectorIndexModel.Render(renderArgs),
1761+
_ => throw new NotSupportedException(
1762+
$"'{model.GetType().Name}' is not a supported index model type.")
1763+
})),
17471764
_collection._messageEncoderSettings);
1765+
}
17481766

17491767
private string[] GetIndexNames(BsonDocument createSearchIndexesResponse) =>
17501768
createSearchIndexesResponse["indexesCreated"]

src/MongoDB.Driver/Search/IMongoSearchIndexManager.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public interface IMongoSearchIndexManager
3333
/// <returns>
3434
/// An <see cref="IEnumerable{String}" /> of the names of the indexes that were created.
3535
/// </returns>
36-
IEnumerable<string> CreateMany(IEnumerable<CreateSearchIndexModel> models, CancellationToken cancellationToken = default);
36+
IEnumerable<string> CreateMany(IEnumerable<CreateSearchIndexModelBase> models, CancellationToken cancellationToken = default);
3737

3838
/// <summary>
3939
/// Creates multiple indexes.
@@ -43,7 +43,7 @@ public interface IMongoSearchIndexManager
4343
/// <returns>
4444
/// A Task whose result is an <see cref="IEnumerable{String}" /> of the names of the indexes that were created.
4545
/// </returns>
46-
Task<IEnumerable<string>> CreateManyAsync(IEnumerable<CreateSearchIndexModel> models, CancellationToken cancellationToken = default);
46+
Task<IEnumerable<string>> CreateManyAsync(IEnumerable<CreateSearchIndexModelBase> models, CancellationToken cancellationToken = default);
4747

4848
/// <summary>
4949
/// Creates a search index.
@@ -64,7 +64,7 @@ public interface IMongoSearchIndexManager
6464
/// <returns>
6565
/// The name of the index that was created.
6666
/// </returns>
67-
string CreateOne(CreateSearchIndexModel model, CancellationToken cancellationToken = default);
67+
string CreateOne(CreateSearchIndexModelBase model, CancellationToken cancellationToken = default);
6868

6969
/// <summary>
7070
/// Creates a search index.
@@ -85,7 +85,7 @@ public interface IMongoSearchIndexManager
8585
/// <returns>
8686
/// A Task whose result is the name of the index that was created.
8787
/// </returns>
88-
Task<string> CreateOneAsync(CreateSearchIndexModel model, CancellationToken cancellationToken = default);
88+
Task<string> CreateOneAsync(CreateSearchIndexModelBase model, CancellationToken cancellationToken = default);
8989

9090
/// <summary>
9191
/// Drops an index by its name.

0 commit comments

Comments
 (0)