diff --git a/src/Momento.Sdk/Internal/VectorIndexControlClient.cs b/src/Momento.Sdk/Internal/VectorIndexControlClient.cs index 57066d9d..7a6c7a9a 100644 --- a/src/Momento.Sdk/Internal/VectorIndexControlClient.cs +++ b/src/Momento.Sdk/Internal/VectorIndexControlClient.cs @@ -71,7 +71,8 @@ public async Task ListIndexesAsync() var response = await grpcManager.Client.ListIndexesAsync(request, new CallOptions(deadline: CalculateDeadline())); return _logger.LogTraceGenericRequestSuccess("listVectorIndexes", new ListIndexesResponse.Success( - new List(response.Indexes.Select(n => new IndexInfo(n.IndexName))))); + new List(response.Indexes.Select(n => new IndexInfo(n.IndexName, (int)n.NumDimensions, Convert(n.SimilarityMetric)))) + )); } catch (Exception e) { @@ -79,6 +80,17 @@ public async Task ListIndexesAsync() } } + private static SimilarityMetric Convert(_SimilarityMetric similarityMetric) + { + return similarityMetric.SimilarityMetricCase switch + { + _SimilarityMetric.SimilarityMetricOneofCase.InnerProduct => SimilarityMetric.InnerProduct, + _SimilarityMetric.SimilarityMetricOneofCase.EuclideanSimilarity => SimilarityMetric.EuclideanSimilarity, + _SimilarityMetric.SimilarityMetricOneofCase.CosineSimilarity => SimilarityMetric.CosineSimilarity, + _ => throw new UnknownException($"Unknown similarity metric {similarityMetric}") + }; + } + public async Task DeleteIndexAsync(string indexName) { try @@ -102,7 +114,7 @@ private static void CheckValidIndexName(string indexName) throw new InvalidArgumentException("Index name must be nonempty"); } } - + private static ulong ValidateNumDimensions(long numDimensions) { if (numDimensions <= 0) diff --git a/src/Momento.Sdk/Responses/Vector/IndexInfo.cs b/src/Momento.Sdk/Responses/Vector/IndexInfo.cs index f3ae12eb..fab1b34e 100644 --- a/src/Momento.Sdk/Responses/Vector/IndexInfo.cs +++ b/src/Momento.Sdk/Responses/Vector/IndexInfo.cs @@ -1,3 +1,5 @@ +using Momento.Sdk.Requests.Vector; + namespace Momento.Sdk.Responses.Vector; /// @@ -10,30 +12,54 @@ public class IndexInfo /// public string Name { get; } + /// + /// The number of dimensions in the index. + /// + public int NumDimensions { get; } + + /// + /// The similarity metric used by the index. + /// + public SimilarityMetric SimilarityMetric { get; } + /// /// Constructs an IndexInfo. /// /// The name of the index. - public IndexInfo(string name) + /// The number of dimensions in the index. + /// The similarity metric used by the index. + public IndexInfo(string name, int numDimensions, SimilarityMetric similarityMetric) { Name = name; + NumDimensions = numDimensions; + SimilarityMetric = similarityMetric; } + /// public override bool Equals(object obj) { - return obj is IndexInfo other && Name == other.Name; + return obj is IndexInfo other && Name == other.Name && NumDimensions == other.NumDimensions && SimilarityMetric == other.SimilarityMetric; } /// public override int GetHashCode() { - return Name.GetHashCode(); + // Overflow is standard here since the alternative is modulo arithmetic, which just wraps around the same. + unchecked + { + // see https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/HashCode.cs#L58 + var hash = 17; + hash = hash * 23 + Name.GetHashCode(); + hash = hash * 23 + NumDimensions.GetHashCode(); + hash = hash * 23 + SimilarityMetric.GetHashCode(); + return hash; + } } /// public override string ToString() { - return $"IndexInfo {{ Name = {Name} }}"; + return $"IndexInfo {{ Name = {Name} NumDimensions = {NumDimensions} SimilarityMetric = {SimilarityMetric} }}"; } -} \ No newline at end of file +} diff --git a/tests/Integration/Momento.Sdk.Tests/Utils.cs b/tests/Integration/Momento.Sdk.Tests/Utils.cs index b63fb044..57b8689f 100644 --- a/tests/Integration/Momento.Sdk.Tests/Utils.cs +++ b/tests/Integration/Momento.Sdk.Tests/Utils.cs @@ -5,9 +5,11 @@ namespace Momento.Sdk.Tests.Integration; /// public static class Utils { - + public static string TestCacheName() => "dotnet-integration-" + NewGuidString(); - + + public static string TestVectorIndexName() => "dotnet-integration-" + NewGuidString(); + public static string NewGuidString() => Guid.NewGuid().ToString(); public static byte[] NewGuidByteArray() => Guid.NewGuid().ToByteArray(); @@ -28,4 +30,4 @@ public static void CreateCacheForTest(ICacheClient cacheClient, string cacheName throw new Exception($"Error when creating cache: {result}"); } } -} \ No newline at end of file +} diff --git a/tests/Integration/Momento.Sdk.Tests/VectorIndexControlTest.cs b/tests/Integration/Momento.Sdk.Tests/VectorIndexControlTest.cs index ad2435e7..f92266c5 100644 --- a/tests/Integration/Momento.Sdk.Tests/VectorIndexControlTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/VectorIndexControlTest.cs @@ -1,37 +1,50 @@ using System.Linq; using System.Threading.Tasks; - using Momento.Sdk.Responses.Vector; +using Momento.Sdk.Responses.Vector; +using Momento.Sdk.Requests.Vector; +using System.Collections.Generic; namespace Momento.Sdk.Tests.Integration; public class VectorIndexControlTest : IClassFixture { private readonly IPreviewVectorIndexClient vectorIndexClient; - + public VectorIndexControlTest(VectorIndexClientFixture vectorIndexFixture) { vectorIndexClient = vectorIndexFixture.Client; } - [Fact] - public async Task CreateListDelete_HappyPath() + public static IEnumerable CreateAndListIndexTestData { - var indexName = $"dotnet-integration-{Utils.NewGuidString()}"; - const int numDimensions = 3; + get + { + return new List + { + new object[] { new IndexInfo(Utils.TestVectorIndexName(), 3, SimilarityMetric.CosineSimilarity) }, + new object[] { new IndexInfo(Utils.TestVectorIndexName(), 3, SimilarityMetric.InnerProduct) }, + new object[] { new IndexInfo(Utils.TestVectorIndexName(), 3, SimilarityMetric.EuclideanSimilarity) } + }; + } + } + [Theory] + [MemberData(nameof(CreateAndListIndexTestData))] + public async Task CreateListDelete_HappyPath(IndexInfo indexInfo) + { try { - var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, numDimensions); + var createResponse = await vectorIndexClient.CreateIndexAsync(indexInfo.Name, indexInfo.NumDimensions, indexInfo.SimilarityMetric); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); var listResponse = await vectorIndexClient.ListIndexesAsync(); Assert.True(listResponse is ListIndexesResponse.Success, $"Unexpected response: {listResponse}"); var listOk = (ListIndexesResponse.Success)listResponse; - Assert.Contains(listOk.Indexes.Select(i => i.Name), name => name == indexName); + Assert.Contains(indexInfo, listOk.Indexes); } finally { - var deleteResponse = await vectorIndexClient.DeleteIndexAsync(indexName); + var deleteResponse = await vectorIndexClient.DeleteIndexAsync(indexInfo.Name); Assert.True(deleteResponse is DeleteIndexResponse.Success, $"Unexpected response: {deleteResponse}"); } } @@ -39,16 +52,16 @@ public async Task CreateListDelete_HappyPath() [Fact] public async Task CreateIndexAsync_AlreadyExistsError() { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); const int numDimensions = 3; - + var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, numDimensions); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); - + var createAgainResponse = await vectorIndexClient.CreateIndexAsync(indexName, numDimensions); Assert.True(createAgainResponse is CreateIndexResponse.AlreadyExists, $"Unexpected response: {createAgainResponse}"); } - + [Fact] public async Task CreateIndexAsync_InvalidIndexName() { @@ -56,13 +69,13 @@ public async Task CreateIndexAsync_InvalidIndexName() Assert.True(createResponse is CreateIndexResponse.Error, $"Unexpected response: {createResponse}"); var createErr = (CreateIndexResponse.Error)createResponse; Assert.Equal(MomentoErrorCode.INVALID_ARGUMENT_ERROR, createErr.InnerException.ErrorCode); - + createResponse = await vectorIndexClient.CreateIndexAsync("", 3); Assert.True(createResponse is CreateIndexResponse.Error, $"Unexpected response: {createResponse}"); createErr = (CreateIndexResponse.Error)createResponse; Assert.Equal(MomentoErrorCode.INVALID_ARGUMENT_ERROR, createErr.InnerException.ErrorCode); } - + [Fact] public async Task CreateIndexAsync_InvalidNumDimensions() { @@ -75,10 +88,10 @@ public async Task CreateIndexAsync_InvalidNumDimensions() [Fact] public async Task DeleteIndexAsync_DoesntExistError() { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var deleteResponse = await vectorIndexClient.DeleteIndexAsync(indexName); Assert.True(deleteResponse is DeleteIndexResponse.Error, $"Unexpected response: {deleteResponse}"); var deleteErr = (DeleteIndexResponse.Error)deleteResponse; Assert.Equal(MomentoErrorCode.NOT_FOUND_ERROR, deleteErr.InnerException.ErrorCode); } -} \ No newline at end of file +} diff --git a/tests/Integration/Momento.Sdk.Tests/VectorIndexDataTest.cs b/tests/Integration/Momento.Sdk.Tests/VectorIndexDataTest.cs index e9a2b9a3..da0e9f9b 100644 --- a/tests/Integration/Momento.Sdk.Tests/VectorIndexDataTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/VectorIndexDataTest.cs @@ -109,7 +109,7 @@ public static IEnumerable UpsertAndSearchTestData public async Task UpsertAndSearch_InnerProduct(SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2, SimilarityMetric.InnerProduct); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); @@ -145,7 +145,7 @@ public async Task UpsertAndSearch_InnerProduct(SearchDelegate searchDelega public async Task UpsertAndSearch_CosineSimilarity(SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); @@ -184,7 +184,7 @@ public async Task UpsertAndSearch_CosineSimilarity(SearchDelegate searchDe public async Task UpsertAndSearch_EuclideanSimilarity(SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2, SimilarityMetric.EuclideanSimilarity); @@ -224,7 +224,7 @@ public async Task UpsertAndSearch_EuclideanSimilarity(SearchDelegate searc public async Task UpsertAndSearch_TopKLimit(SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2, SimilarityMetric.InnerProduct); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); @@ -266,7 +266,7 @@ public async Task UpsertAndSearch_TopKLimit(SearchDelegate searchDelegate, public async Task UpsertAndSearch_WithMetadata(SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2, SimilarityMetric.InnerProduct); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); @@ -346,7 +346,7 @@ public async Task UpsertAndSearch_WithMetadata(SearchDelegate searchDelega public async Task UpsertAndSearch_WithDiverseMetadata(SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2, SimilarityMetric.InnerProduct); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); @@ -410,7 +410,7 @@ public async Task UpsertAndSearch_WithDiverseMetadata(SearchDelegate searc new List { 3.0f, 20.0f, -0.01f } } }; - + // Combine the search threshold parameters and the search/search with vectors parameters public static IEnumerable UpsertAndSearchThresholdTestCases => SearchThresholdTestCases.SelectMany( @@ -422,7 +422,7 @@ public async Task UpsertAndSearch_WithDiverseMetadata(SearchDelegate searc public async Task Search_PruneBasedOnThreshold(SimilarityMetric similarityMetric, List scores, List thresholds, SearchDelegate searchDelegate, AssertOnSearchResponse assertOnSearchResponse) { - var indexName = $"index-{Utils.NewGuidString()}"; + var indexName = Utils.TestVectorIndexName(); var createResponse = await vectorIndexClient.CreateIndexAsync(indexName, 2, similarityMetric); Assert.True(createResponse is CreateIndexResponse.Success, $"Unexpected response: {createResponse}"); @@ -472,4 +472,4 @@ public async Task Search_PruneBasedOnThreshold(SimilarityMetric similarityMet await vectorIndexClient.DeleteIndexAsync(indexName); } } -} \ No newline at end of file +}