From a2be468cbe32a06b231357e80193170c966e9f5a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 11 Aug 2025 14:46:13 +0100 Subject: [PATCH 01/32] Iniial stab at vector-set API --- .../Interfaces/IDatabase.cs | 208 ++++++++++++ .../Interfaces/IDatabaseAsync.cs | 81 +++++ .../KeyspaceIsolation/KeyPrefixed.cs | 80 +++++ .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 80 +++++ .../PublicAPI/PublicAPI.Unshipped.txt | 55 ++++ src/StackExchange.Redis/RedisDatabase.cs | 220 +++++++++++++ .../VectorQuantizationType.cs | 27 ++ src/StackExchange.Redis/VectorSetInfo.cs | 40 +++ src/StackExchange.Redis/VectorSetLink.cs | 18 + .../VectorSimilarityResult.cs | 23 ++ .../KeyPrefixedVectorSetTests.cs | 243 ++++++++++++++ .../VectorSetIntegrationTests.cs | 310 ++++++++++++++++++ 12 files changed, 1385 insertions(+) create mode 100644 src/StackExchange.Redis/VectorQuantizationType.cs create mode 100644 src/StackExchange.Redis/VectorSetInfo.cs create mode 100644 src/StackExchange.Redis/VectorSetLink.cs create mode 100644 src/StackExchange.Redis/VectorSimilarityResult.cs create mode 100644 tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs create mode 100644 tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index b6caafabe..98637ad8b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3428,5 +3428,213 @@ IEnumerable SortedSetScan( /// The length of the string after it was modified by the command. /// RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); + + // Vector Set operations + + /// + /// Add a vector to a vectorset. + /// + /// The key of the vectorset. + /// The element name. + /// The vector data. + /// Optional dimension reduction using random projection (REDUCE parameter). + /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. + /// Optional HNSW build exploration factor (EF parameter, default: 200). + /// Optional maximum connections per HNSW node (M parameter, default: 16). + /// Optional check-and-set mode for partial threading (CAS parameter). + /// Optional JSON attributes for the element (SETATTR parameter). + /// The flags to use for this operation. + /// The number of elements added to the vectorset. + /// + long VectorSetAdd( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None); + + /// + /// Get the cardinality (number of elements) of a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// The cardinality of the vectorset. + /// + long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get the dimension of vectors in a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// The dimension of vectors in the vectorset. + /// + int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get the vector for a member. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The vector as a pooled memory lease. + /// + Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get JSON attributes for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The attributes as a JSON string. + /// + string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get information about a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// Information about the vectorset. + /// + VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Check if a member exists in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// True if the member exists, false otherwise. + /// + bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get links/connections for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The linked members. + /// + Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get links/connections with scores for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The linked members with their similarity scores. + /// + Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get a random member from a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// A random member from the vectorset, or null if the vectorset is empty. + /// + RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get random members from a vectorset. + /// + /// The key of the vectorset. + /// The number of random members to return. + /// The flags to use for this operation. + /// Random members from the vectorset. + /// + RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Remove a member from a vectorset. + /// + /// The key of the vectorset. + /// The member to remove. + /// The flags to use for this operation. + /// The number of members removed. + /// + long VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Set JSON attributes for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The attributes to set as a JSON string. + /// The flags to use for this operation. + /// True if successful. + /// + bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); + + /// + /// Find similar vectors using vector similarity search. + /// + /// The key of the vectorset. + /// The query vector. + /// The number of similar vectors to return (COUNT parameter). + /// Whether to include similarity scores in the results (WITHSCORES parameter). + /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). + /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). + /// Optional search exploration factor for better recall (EF parameter). + /// Optional filter expression to restrict results (FILTER parameter). + /// Optional maximum filtering attempts (FILTER-EF parameter). + /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). + /// Whether to run search in main thread (NOTHREAD parameter). + /// The flags to use for this operation. + /// Similar vectors with their similarity scores. + /// + Lease? VectorSetSimilaritySearchByVector( + RedisKey key, + ReadOnlyMemory vector, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); + + /// + /// Find similar vectors to an existing member. + /// + /// The key of the vectorset. + /// The member to find similar vectors for. + /// The number of similar vectors to return (COUNT parameter). + /// Whether to include similarity scores in the results (WITHSCORES parameter). + /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). + /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). + /// Optional search exploration factor for better recall (EF parameter). + /// Optional filter expression to restrict results (FILTER parameter). + /// Optional maximum filtering attempts (FILTER-EF parameter). + /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). + /// Whether to run search in main thread (NOTHREAD parameter). + /// The flags to use for this operation. + /// Similar vectors with their similarity scores. + /// + Lease? VectorSetSimilaritySearchByMember( + RedisKey key, + RedisValue member, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 51a15d7d5..fde5d8b89 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -843,5 +843,86 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); + + // Vector Set operations + + /// + Task VectorSetAddAsync( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); + + /// + Task?> VectorSetSimilaritySearchByVectorAsync( + RedisKey key, + ReadOnlyMemory vector, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); + + /// + Task?> VectorSetSimilaritySearchByMemberAsync( + RedisKey key, + RedisValue member, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 32c76f4d2..e682dfe2f 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -814,6 +814,86 @@ public void Wait(Task task) => public void WaitAll(params Task[] tasks) => Inner.WaitAll(tasks); + // Vector Set operations - async methods + public Task VectorSetAddAsync( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetAddAsync(ToInner(key), element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetLengthAsync(ToInner(key), flags); + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetDimensionAsync(ToInner(key), flags); + + public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetApproximateVectorAsync(ToInner(key), member, flags); + + public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetAttributesJsonAsync(ToInner(key), member, flags); + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetInfoAsync(ToInner(key), flags); + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetContainsAsync(ToInner(key), member, flags); + + public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksAsync(ToInner(key), member, flags); + + public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksWithScoresAsync(ToInner(key), member, flags); + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMemberAsync(ToInner(key), flags); + + public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMembersAsync(ToInner(key), count, flags); + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRemoveAsync(ToInner(key), member, flags); + + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, jsonAttributes, flags); + + public Task?> VectorSetSimilaritySearchByVectorAsync( + RedisKey key, + ReadOnlyMemory vector, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByVectorAsync(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + + public Task?> VectorSetSimilaritySearchByMemberAsync( + RedisKey key, + RedisValue member, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByMemberAsync(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + protected internal RedisKey ToInner(RedisKey outer) => RedisKey.WithPrefix(Prefix, outer); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 755bec64e..44f53f700 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -804,5 +804,85 @@ public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyTouch(ToInner(keys), flags); + + // Vector Set operations + public long VectorSetAdd( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetAdd(ToInner(key), element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetLength(ToInner(key), flags); + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetDimension(ToInner(key), flags); + + public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetApproximateVector(ToInner(key), member, flags); + + public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetAttributesJson(ToInner(key), member, flags); + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetInfo(ToInner(key), flags); + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetContains(ToInner(key), member, flags); + + public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinks(ToInner(key), member, flags); + + public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksWithScores(ToInner(key), member, flags); + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMember(ToInner(key), flags); + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMembers(ToInner(key), count, flags); + + public long VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRemove(ToInner(key), member, flags); + + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJson(ToInner(key), member, jsonAttributes, flags); + + public Lease? VectorSetSimilaritySearchByVector( + RedisKey key, + ReadOnlyMemory vector, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByVector(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + + public Lease? VectorSetSimilaritySearchByMember( + RedisKey key, + RedisValue member, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByMember(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62..1377080d1 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,56 @@ #nullable enable +StackExchange.Redis.VectorQuantizationType +StackExchange.Redis.VectorQuantizationType.Binary = 3 -> StackExchange.Redis.VectorQuantizationType +StackExchange.Redis.VectorQuantizationType.Int8 = 2 -> StackExchange.Redis.VectorQuantizationType +StackExchange.Redis.VectorQuantizationType.None = 1 -> StackExchange.Redis.VectorQuantizationType +StackExchange.Redis.VectorQuantizationType.Unknown = 0 -> StackExchange.Redis.VectorQuantizationType +StackExchange.Redis.VectorSetInfo +StackExchange.Redis.VectorSetInfo.Dimension.get -> int +StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long +StackExchange.Redis.VectorSetInfo.Length.get -> long +StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int +StackExchange.Redis.VectorSetInfo.QuantizationType.get -> StackExchange.Redis.VectorQuantizationType +StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void +StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorQuantizationType quantizationType, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void +StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long +StackExchange.Redis.VectorSetLink +StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.VectorSetLink.Score.get -> double +StackExchange.Redis.VectorSetLink.VectorSetLink() -> void +StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void +StackExchange.Redis.VectorSimilarityResult +StackExchange.Redis.VectorSimilarityResult.AttributesJson.get -> string? +StackExchange.Redis.VectorSimilarityResult.Member.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.VectorSimilarityResult.Score.get -> double +StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult() -> void +StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void +StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int +StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.VectorSetGetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +StackExchange.Redis.IDatabase.VectorSetGetLinks(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.VectorSetGetLinksWithScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.VectorSetInfo? +StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.VectorSetGetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksWithScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index bf69f25f3..2946fb8f9 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5721,5 +5721,225 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } } + + // Vector Set operations - stub implementations (to be implemented) + public long VectorSetAdd( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public long VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Lease? VectorSetSimilaritySearchByVector( + RedisKey key, + ReadOnlyMemory vector, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Lease? VectorSetSimilaritySearchByMember( + RedisKey key, + RedisValue member, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + // Vector Set async operations + public Task VectorSetAddAsync( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task?> VectorSetSimilaritySearchByVectorAsync( + RedisKey key, + ReadOnlyMemory vector, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } + + public Task?> VectorSetSimilaritySearchByMemberAsync( + RedisKey key, + RedisValue member, + long count = 10, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + throw new NotImplementedException("Vector Set operations are not yet implemented"); + } } } diff --git a/src/StackExchange.Redis/VectorQuantizationType.cs b/src/StackExchange.Redis/VectorQuantizationType.cs new file mode 100644 index 000000000..666898be8 --- /dev/null +++ b/src/StackExchange.Redis/VectorQuantizationType.cs @@ -0,0 +1,27 @@ +namespace StackExchange.Redis; + +/// +/// Specifies the quantization type for vectors in a vectorset. +/// +public enum VectorQuantizationType +{ + /// + /// Unknown or unrecognized quantization type. + /// + Unknown, + + /// + /// No quantization (full precision). + /// + None, + + /// + /// 8-bit integer quantization (default). + /// + Int8, + + /// + /// Binary quantization. + /// + Binary, +} diff --git a/src/StackExchange.Redis/VectorSetInfo.cs b/src/StackExchange.Redis/VectorSetInfo.cs new file mode 100644 index 000000000..78a2557a8 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetInfo.cs @@ -0,0 +1,40 @@ +using System; + +namespace StackExchange.Redis +{ + /// + /// Contains metadata information about a vectorset returned by VINFO command. + /// + public readonly struct VectorSetInfo(VectorQuantizationType quantizationType, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) + { + /// + /// The quantization type used for vectors in this vectorset. + /// + public VectorQuantizationType QuantizationType { get; } = quantizationType; + + /// + /// The number of dimensions in each vector. + /// + public int Dimension { get; } = dimension; + + /// + /// The number of elements (cardinality) in the vectorset. + /// + public long Length { get; } = length; + + /// + /// The maximum level in the HNSW graph structure. + /// + public int MaxLevel { get; } = maxLevel; + + /// + /// The unique identifier for this vectorset. + /// + public long VectorSetUid { get; } = vectorSetUid; + + /// + /// The maximum node unique identifier in the HNSW graph. + /// + public long HnswMaxNodeUid { get; } = hnswMaxNodeUid; + } +} diff --git a/src/StackExchange.Redis/VectorSetLink.cs b/src/StackExchange.Redis/VectorSetLink.cs new file mode 100644 index 000000000..b2256c10d --- /dev/null +++ b/src/StackExchange.Redis/VectorSetLink.cs @@ -0,0 +1,18 @@ +namespace StackExchange.Redis; + +/// +/// Represents a link/connection between members in a vectorset with similarity score. +/// Used by VLINKS command with WITHSCORES option. +/// +public readonly struct VectorSetLink(RedisValue member, double score) +{ + /// + /// The linked member name/identifier. + /// + public RedisValue Member { get; } = member; + + /// + /// The similarity score between the queried member and this linked member. + /// + public double Score { get; } = score; +} diff --git a/src/StackExchange.Redis/VectorSimilarityResult.cs b/src/StackExchange.Redis/VectorSimilarityResult.cs new file mode 100644 index 000000000..0a8a8826a --- /dev/null +++ b/src/StackExchange.Redis/VectorSimilarityResult.cs @@ -0,0 +1,23 @@ +namespace StackExchange.Redis; + +/// +/// Represents a result from vector similarity search operations. +/// +public readonly struct VectorSimilarityResult(RedisValue member, double score = double.NaN, string? attributesJson = null) +{ + /// + /// The member name/identifier in the vectorset. + /// + public RedisValue Member { get; } = member; + + /// + /// The similarity score (0-1) when WITHSCORES is used, NaN otherwise. + /// A score of 1 means identical vectors, 0 means opposite vectors. + /// + public double Score { get; } = score; + + /// + /// The JSON attributes associated with the member when WITHATTRIBS is used, null otherwise. + /// + public string? AttributesJson { get; } = attributesJson; +} diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs new file mode 100644 index 000000000..d970168c2 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -0,0 +1,243 @@ +using System; +using System.Text; +using NSubstitute; +using Xunit; + +namespace StackExchange.Redis.Tests +{ + [Collection(nameof(SubstituteDependentCollection))] + public sealed class KeyPrefixedVectorSetTests + { + private readonly IDatabase mock; + private readonly IDatabase prefixed; + + public KeyPrefixedVectorSetTests() + { + mock = Substitute.For(); + prefixed = new KeyspaceIsolation.KeyPrefixedDatabase(mock, Encoding.UTF8.GetBytes("prefix:")); + } + + [Fact] + public void VectorSetAdd_BasicCall() + { + var vector = new float[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + + prefixed.VectorSetAdd("vectorset", "element1", vector); + + mock.Received().VectorSetAdd( + "prefix:vectorset", + "element1", + vector, + null, + VectorQuantizationType.Int8, + null, + null, + false, + null, + CommandFlags.None); + } + + [Fact] + public void VectorSetAdd_WithAllParameters() + { + var vector = new float[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + var attributes = """{"category":"test"}"""; + + prefixed.VectorSetAdd( + "vectorset", + "element1", + vector, + reducedDimensions: 64, + quantizationType: VectorQuantizationType.Binary, + buildExplorationFactor: 300, + maxConnections: 32, + useCheckAndSet: true, + attributesJson: attributes, + flags: CommandFlags.FireAndForget); + + mock.Received().VectorSetAdd( + "prefix:vectorset", + "element1", + vector, + 64, + VectorQuantizationType.Binary, + 300, + 32, + true, + attributes, + CommandFlags.FireAndForget); + } + + [Fact] + public void VectorSetLength() + { + prefixed.VectorSetLength("vectorset", CommandFlags.None); + mock.Received().VectorSetLength("prefix:vectorset", CommandFlags.None); + } + + [Fact] + public void VectorSetDimension() + { + prefixed.VectorSetDimension("vectorset", CommandFlags.None); + mock.Received().VectorSetDimension("prefix:vectorset", CommandFlags.None); + } + + [Fact] + public void VectorSetGetApproximateVector() + { + prefixed.VectorSetGetApproximateVector("vectorset", "member1", CommandFlags.None); + mock.Received().VectorSetGetApproximateVector("prefix:vectorset", "member1", CommandFlags.None); + } + + [Fact] + public void VectorSetGetAttributesJson() + { + prefixed.VectorSetGetAttributesJson("vectorset", "member1", CommandFlags.None); + mock.Received().VectorSetGetAttributesJson("prefix:vectorset", "member1", CommandFlags.None); + } + + [Fact] + public void VectorSetInfo() + { + prefixed.VectorSetInfo("vectorset", CommandFlags.None); + mock.Received().VectorSetInfo("prefix:vectorset", CommandFlags.None); + } + + [Fact] + public void VectorSetContains() + { + prefixed.VectorSetContains("vectorset", "member1", CommandFlags.None); + mock.Received().VectorSetContains("prefix:vectorset", "member1", CommandFlags.None); + } + + [Fact] + public void VectorSetGetLinks() + { + prefixed.VectorSetGetLinks("vectorset", "member1", CommandFlags.None); + mock.Received().VectorSetGetLinks("prefix:vectorset", "member1", CommandFlags.None); + } + + [Fact] + public void VectorSetGetLinksWithScores() + { + prefixed.VectorSetGetLinksWithScores("vectorset", "member1", CommandFlags.None); + mock.Received().VectorSetGetLinksWithScores("prefix:vectorset", "member1", CommandFlags.None); + } + + [Fact] + public void VectorSetRandomMember() + { + prefixed.VectorSetRandomMember("vectorset", CommandFlags.None); + mock.Received().VectorSetRandomMember("prefix:vectorset", CommandFlags.None); + } + + [Fact] + public void VectorSetRandomMembers() + { + prefixed.VectorSetRandomMembers("vectorset", 5, CommandFlags.None); + mock.Received().VectorSetRandomMembers("prefix:vectorset", 5, CommandFlags.None); + } + + [Fact] + public void VectorSetRemove() + { + prefixed.VectorSetRemove("vectorset", "member1", CommandFlags.None); + mock.Received().VectorSetRemove("prefix:vectorset", "member1", CommandFlags.None); + } + + [Fact] + public void VectorSetSetAttributesJson() + { + var attributes = """{"category":"test"}"""; + + prefixed.VectorSetSetAttributesJson("vectorset", "member1", attributes, CommandFlags.None); + mock.Received().VectorSetSetAttributesJson("prefix:vectorset", "member1", attributes, CommandFlags.None); + } + + [Fact] + public void VectorSetSimilaritySearchByVector() + { + var vector = new float[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + + prefixed.VectorSetSimilaritySearchByVector( + "vectorset", + vector, + 10, + false, + false, + null, + null, + null, + null, + false, + false, + CommandFlags.None); + mock.Received().VectorSetSimilaritySearchByVector( + "prefix:vectorset", + vector, + 10, + false, + false, + null, + null, + null, + null, + false, + false, + CommandFlags.None); + } + + [Fact] + public void VectorSetSimilaritySearchByMember() + { + prefixed.VectorSetSimilaritySearchByMember( + "vectorset", + "member1", + 5, + true, + true, + 0.1, + 400, + "category='test'", + 1000, + true, + true, + CommandFlags.FireAndForget); + mock.Received().VectorSetSimilaritySearchByMember( + "prefix:vectorset", + "member1", + 5, + true, + true, + 0.1, + 400, + "category='test'", + 1000, + true, + true, + CommandFlags.FireAndForget); + } + + [Fact] + public void VectorSetSimilaritySearchByVector_DefaultParameters() + { + var vector = new float[] { 1.0f, 2.0f }.AsMemory(); + + // Test that default parameters work correctly + prefixed.VectorSetSimilaritySearchByVector("vectorset", vector); + mock.Received().VectorSetSimilaritySearchByVector( + "prefix:vectorset", + vector, + 10, + false, + false, + null, + null, + null, + null, + false, + false, + CommandFlags.None); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs new file mode 100644 index 000000000..279fbbf2f --- /dev/null +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -0,0 +1,310 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +public sealed class VectorSetIntegrationTests : TestBase +{ + public VectorSetIntegrationTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task VectorSetAdd_BasicOperation() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + // Clean up any existing data + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + Assert.Equal(1, result); + } + + [Fact] + public async Task VectorSetAdd_WithAttributes() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var attributes = """{"category":"test","id":123}"""; + + var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), attributesJson: attributes); + + Assert.Equal(1, result); + + // Verify attributes were stored + var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); + Assert.Equal(attributes, retrievedAttributes); + } + + [Fact] + public async Task VectorSetLength_EmptySet() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var length = await db.VectorSetLengthAsync(key); + Assert.Equal(0, length); + } + + [Fact] + public async Task VectorSetLength_WithElements() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + + var length = await db.VectorSetLengthAsync(key); + Assert.Equal(2, length); + } + + [Fact] + public async Task VectorSetDimension() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + var dimension = await db.VectorSetDimensionAsync(key); + Assert.Equal(5, dimension); + } + + [Fact] + public async Task VectorSetContains() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f }; + await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + var exists = await db.VectorSetContainsAsync(key, "element1"); + var notExists = await db.VectorSetContainsAsync(key, "element2"); + + Assert.True(exists); + Assert.False(notExists); + } + + [Fact] + public async Task VectorSetGetApproximateVector() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var originalVector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + await db.VectorSetAddAsync(key, "element1", originalVector.AsMemory()); + + using var retrievedLease = await db.VectorSetGetApproximateVectorAsync(key, "element1"); + + Assert.NotNull(retrievedLease); + var retrievedVector = retrievedLease.Span; + + Assert.Equal(originalVector.Length, retrievedVector.Length); + // Note: Due to quantization, values might not be exactly equal + for (int i = 0; i < originalVector.Length; i++) + { + var delta = Math.Abs(originalVector[i] - retrievedVector[i]); + Assert.True(delta < 0.1f, $"Vector component {i} differs too much: expected {originalVector[i]}, got {retrievedVector[i]}"); + } + } + + [Fact] + public async Task VectorSetRemove() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f }; + await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + var removed = await db.VectorSetRemoveAsync(key, "element1"); + Assert.Equal(1, removed); + + var exists = await db.VectorSetContainsAsync(key, "element1"); + Assert.False(exists); + } + + [Fact] + public async Task VectorSetInfo() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + var info = await db.VectorSetInfoAsync(key); + + Assert.NotNull(info); + Assert.Equal(5, info.Value.Dimension); + Assert.Equal(1, info.Value.Length); + Assert.Equal(VectorQuantizationType.Int8, info.Value.QuantizationType); + } + + [Fact] + public async Task VectorSetRandomMember() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + + var randomMember = await db.VectorSetRandomMemberAsync(key); + Assert.True(randomMember == "element1" || randomMember == "element2"); + } + + [Fact] + public async Task VectorSetRandomMembers() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; + var vector3 = new float[] { 7.0f, 8.0f, 9.0f }; + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + + var randomMembers = await db.VectorSetRandomMembersAsync(key, 2); + + Assert.Equal(2, randomMembers.Length); + Assert.All(randomMembers, member => + Assert.True(member == "element1" || member == "element2" || member == "element3")); + } + + [Fact] + public async Task VectorSetSimilaritySearch_WithVector() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + // Add some test vectors + var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; + var vector3 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + + // Search for vectors similar to vector1 + using var results = + await db.VectorSetSimilaritySearchByVectorAsync(key, vector1.AsMemory(), count: 2, withScores: true); + + Assert.NotNull(results); + var resultsArray = results.Span.ToArray(); + + Assert.True(resultsArray.Length <= 2); + Assert.Contains(resultsArray, r => r.Member == "element1"); + + // Verify scores are present when withScores is true + Assert.All(resultsArray, r => Assert.False(double.IsNaN(r.Score))); + } + + [Fact] + public async Task VectorSetSimilaritySearch_WithMember() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + + using var results = + await db.VectorSetSimilaritySearchByMemberAsync(key, "element1", count: 1, withScores: true); + + Assert.NotNull(results); + var resultsArray = results.Span.ToArray(); + + Assert.Single(resultsArray); + Assert.Equal("element1", resultsArray[0].Member); + Assert.False(double.IsNaN(resultsArray[0].Score)); + } + + [Fact] + public async Task VectorSetSimilaritySearch_WithAttributes() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f }; + var attributes = """{"category":"test","priority":"high"}"""; + + await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), attributesJson: attributes); + + using var results = await db.VectorSetSimilaritySearchByVectorAsync( + key, + vector.AsMemory(), + count: 1, + withScores: true, + withAttributes: true); + + Assert.NotNull(results); + var result = results.Span[0]; + + Assert.Equal("element1", result.Member); + Assert.False(double.IsNaN(result.Score)); + Assert.Equal(attributes, result.AttributesJson); + } +} From e8b580e2e732041456e8c4714b6f5576d67bc526 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 11 Aug 2025 14:55:04 +0100 Subject: [PATCH 02/32] Use `bool` as the return from `VADD` --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Interfaces/IDatabase.cs | 4 ++-- src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs | 2 +- src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs | 2 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 2 +- src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt | 4 ++-- src/StackExchange.Redis/RedisDatabase.cs | 4 ++-- tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs | 4 ++-- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3a50f1ef3..39a3bf9c2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -13,6 +13,7 @@ Current package versions: - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) +- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939)) ## 2.8.58 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 98637ad8b..32e3b5b09 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3444,9 +3444,9 @@ IEnumerable SortedSetScan( /// Optional check-and-set mode for partial threading (CAS parameter). /// Optional JSON attributes for the element (SETATTR parameter). /// The flags to use for this operation. - /// The number of elements added to the vectorset. + /// if the element was added; if it already existed. /// - long VectorSetAdd( + bool VectorSetAdd( RedisKey key, RedisValue element, ReadOnlyMemory values, diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index fde5d8b89..d5168a31b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -847,7 +847,7 @@ IAsyncEnumerable SortedSetScanAsync( // Vector Set operations /// - Task VectorSetAddAsync( + Task VectorSetAddAsync( RedisKey key, RedisValue element, ReadOnlyMemory values, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index e682dfe2f..d004d5672 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -815,7 +815,7 @@ public void WaitAll(params Task[] tasks) => Inner.WaitAll(tasks); // Vector Set operations - async methods - public Task VectorSetAddAsync( + public Task VectorSetAddAsync( RedisKey key, RedisValue element, ReadOnlyMemory values, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 44f53f700..ff94e3bc2 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -806,7 +806,7 @@ public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyTouch(ToInner(keys), flags); // Vector Set operations - public long VectorSetAdd( + public bool VectorSetAdd( RedisKey key, RedisValue element, ReadOnlyMemory values, diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 1377080d1..9fca161b3 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -24,7 +24,7 @@ StackExchange.Redis.VectorSimilarityResult.Member.get -> StackExchange.Redis.Red StackExchange.Redis.VectorSimilarityResult.Score.get -> double StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult() -> void StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void -StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? @@ -39,7 +39,7 @@ StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 2946fb8f9..59ffacc66 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5723,7 +5723,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } // Vector Set operations - stub implementations (to be implemented) - public long VectorSetAdd( + public bool VectorSetAdd( RedisKey key, RedisValue element, ReadOnlyMemory values, @@ -5833,7 +5833,7 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j } // Vector Set async operations - public Task VectorSetAddAsync( + public Task VectorSetAddAsync( RedisKey key, RedisValue element, ReadOnlyMemory values, diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 279fbbf2f..6c9aff187 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -22,7 +22,7 @@ public async Task VectorSetAdd_BasicOperation() var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); - Assert.Equal(1, result); + Assert.True(result); } [Fact] @@ -39,7 +39,7 @@ public async Task VectorSetAdd_WithAttributes() var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), attributesJson: attributes); - Assert.Equal(1, result); + Assert.True(result); // Verify attributes were stored var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); From a7a2f6aa5ce9cdcc39120e8a15e50c93ee0b14e0 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 11 Aug 2025 17:05:17 +0100 Subject: [PATCH 03/32] working on impl --- StackExchange.Redis.sln.DotSettings | 3 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 25 ++++ .../Interfaces/IDatabase.cs | 5 +- .../Interfaces/IDatabaseAsync.cs | 3 +- .../KeyspaceIsolation/KeyPrefixed.cs | 2 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 2 +- src/StackExchange.Redis/Message.cs | 1 + src/StackExchange.Redis/PhysicalConnection.cs | 8 ++ .../PublicAPI/PublicAPI.Unshipped.txt | 4 +- src/StackExchange.Redis/RedisDatabase.cs | 64 ++++++---- src/StackExchange.Redis/ResultProcessor.cs | 64 ++++++++++ .../VectorSetAddMessage.cs | 120 ++++++++++++++++++ .../KeyPrefixedVectorSetTests.cs | 120 ++++++++---------- .../VectorSetIntegrationTests.cs | 81 ++++++++---- 14 files changed, 380 insertions(+), 122 deletions(-) create mode 100644 src/StackExchange.Redis/VectorSetAddMessage.cs diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index de893e54d..b72a49d2c 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,4 +1,5 @@  OK PONG - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 52f0b134d..7a0c2f08d 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -206,6 +206,19 @@ internal enum RedisCommand UNSUBSCRIBE, UNWATCH, + VADD, + VCARD, + VDIM, + VEMB, + VGETATTR, + VINFO, + VISMEMBER, + VLINKS, + VRANDMEMBER, + VREM, + VSETATTR, + VSIM, + WATCH, XACK, @@ -352,6 +365,9 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SWAPDB: case RedisCommand.TOUCH: case RedisCommand.UNLINK: + case RedisCommand.VADD: + case RedisCommand.VREM: + case RedisCommand.VSETATTR: case RedisCommand.XAUTOCLAIM: case RedisCommand.ZADD: case RedisCommand.ZDIFFSTORE: @@ -499,6 +515,15 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.ZSCORE: case RedisCommand.ZUNION: case RedisCommand.UNKNOWN: + case RedisCommand.VCARD: + case RedisCommand.VDIM: + case RedisCommand.VEMB: + case RedisCommand.VGETATTR: + case RedisCommand.VINFO: + case RedisCommand.VISMEMBER: + case RedisCommand.VLINKS: + case RedisCommand.VRANDMEMBER: + case RedisCommand.VSIM: // Writable commands, but allowed for the writable-replicas scenario case RedisCommand.COPY: case RedisCommand.GEOADD: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 32e3b5b09..2021d1005 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Net; +// ReSharper disable once CheckNamespace namespace StackExchange.Redis { /// @@ -3560,9 +3561,9 @@ bool VectorSetAdd( /// The key of the vectorset. /// The member to remove. /// The flags to use for this operation. - /// The number of members removed. + /// if the member was removed; if it was not found. /// - long VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// /// Set JSON attributes for a member in a vectorset. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index d5168a31b..3c11e2e79 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -4,6 +4,7 @@ using System.Net; using System.Threading.Tasks; +// ReSharper disable once CheckNamespace namespace StackExchange.Redis { /// @@ -890,7 +891,7 @@ Task VectorSetAddAsync( Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index d004d5672..f640a0b68 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -858,7 +858,7 @@ public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags fl public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.VectorSetRandomMembersAsync(ToInner(key), count, flags); - public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.VectorSetRemoveAsync(ToInner(key), member, flags); public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index ff94e3bc2..164b61457 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -849,7 +849,7 @@ public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = Comma public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.VectorSetRandomMembers(ToInner(key), count, flags); - public long VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.VectorSetRemove(ToInner(key), member, flags); public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index cd3d29947..5973bd55b 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -705,6 +705,7 @@ internal void SetWriteTime() _writeTickCount = Environment.TickCount; // note this might be reset if we resend a message, cluster-moved etc; I'm OK with that } private int _writeTickCount; + public int GetWriteTime() => Volatile.Read(ref _writeTickCount); /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index c587241a0..129fd9e07 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -859,6 +859,14 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullW } } + internal void WriteBulkString(ReadOnlySpan value) + { + if (_ioPipe?.Output is { } writer) + { + WriteUnifiedSpan(writer, value); + } + } + internal const int REDIS_MAX_ARGS = 1024 * 1024; // there is a <= 1024*1024 max constraint inside redis itself: https://github.com/antirez/redis/blob/6c60526db91e23fb2d666fc52facc9a11780a2a3/src/networking.c#L1024 internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 9fca161b3..3d583e69e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -35,7 +35,7 @@ StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, St StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! -StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? @@ -50,7 +50,7 @@ StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisK StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 59ffacc66..82c1b7537 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5735,27 +5735,32 @@ public bool VectorSetAdd( string? attributesJson = null, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + return ExecuteSync(msg, ResultProcessor.Boolean); } public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); + return ExecuteSync(msg, ResultProcessor.Int64); } public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); + return ExecuteSync(msg, ResultProcessor.Int32); } public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); + return ExecuteSync(msg, ResultProcessor.LeaseFloat32); } public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); + return ExecuteSync(msg, ResultProcessor.String); } public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -5765,7 +5770,8 @@ public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.No public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); + return ExecuteSync(msg, ResultProcessor.Boolean); } public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -5780,22 +5786,26 @@ public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flag public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); + return ExecuteSync(msg, ResultProcessor.RedisValue); } public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - public long VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); + return ExecuteSync(msg, ResultProcessor.Boolean); } public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); + return ExecuteSync(msg, ResultProcessor.Boolean); } public Lease? VectorSetSimilaritySearchByVector( @@ -5845,27 +5855,32 @@ public Task VectorSetAddAsync( string? attributesJson = null, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + return ExecuteAsync(msg, ResultProcessor.Boolean); } public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); + return ExecuteAsync(msg, ResultProcessor.Int64); } public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); + return ExecuteAsync(msg, ResultProcessor.Int32); } public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); + return ExecuteAsync(msg, ResultProcessor.LeaseFloat32); } public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); + return ExecuteAsync(msg, ResultProcessor.String); } public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -5875,7 +5890,8 @@ public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = Comm public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); + return ExecuteAsync(msg, ResultProcessor.Boolean); } public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -5890,22 +5906,26 @@ public Task VectorSetContainsAsync(RedisKey key, RedisValue member, Comman public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); + return ExecuteAsync(msg, ResultProcessor.RedisValue); } public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); + return ExecuteAsync(msg, ResultProcessor.Boolean); } public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); + return ExecuteAsync(msg, ResultProcessor.Boolean); } public Task?> VectorSetSimilaritySearchByVectorAsync( diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 627953941..f326fe56e 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -60,6 +60,9 @@ public static readonly ResultProcessor PubSubNumSub = new PubSubNumSubProcessor(), Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1); + public static readonly ResultProcessor Int32 = new Int32Processor(); + public static readonly ResultProcessor?> LeaseFloat32 = new LeaseFloat32Processor(); + public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -1384,6 +1387,26 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private class Int32Processor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + if (result.TryGetInt64(out long i64)) + { + SetResult(message, checked((int)i64)); + return true; + } + break; + } + return false; + } + } + internal static ResultProcessor StreamTrimResult => Int32EnumProcessor.Instance; @@ -2093,6 +2116,47 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class LeaseFloat32Processor : ResultProcessor?> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeArray) + { + case ResultType.Array: + if (result.IsNull) + { + SetResult(message, null); + return true; + } + + var items = result.GetItems(); + if (items.IsEmpty) + { + SetResult(message, Lease.Empty); + return true; + } + + var length = checked((int)items.Length); + var lease = Lease.Create(length, clear: false); + var target = lease.Span; + int index = 0; + foreach (ref RawResult item in items) + { + if (!item.TryGetDouble(out double val)) break; + target[index++] = (float)val; + } + if (index == length) + { + SetResult(message, lease); + return true; + } + lease.Dispose(); // something went wrong; recycle + break; + } + return false; + } + } + private sealed class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs new file mode 100644 index 000000000..847b507a5 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetAddMessage.cs @@ -0,0 +1,120 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace StackExchange.Redis; + +internal sealed class VectorSetAddMessage( + int database, + CommandFlags flags, + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions, + VectorQuantizationType quantizationType, + int? buildExplorationFactor, + int? maxConnections, + bool useCheckAndSet, + string? attributesJson) : Message(database, flags, RedisCommand.VADD) +{ + private static readonly bool CanUseFp32 = BitConverter.IsLittleEndian && CheckFp32(); + private static bool CheckFp32() // check endianness with a known value + { + // ReSharper disable once CompareOfFloatsByEqualityOperator - expect exact + return MemoryMarshal.Cast("\0\0(B"u8)[0] == 42; + } +#if DEBUG + private static int _fp32Disabled; + internal static bool UseFp32 => CanUseFp32 & Volatile.Read(ref _fp32Disabled) == 0; + internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled); + internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled); +#else + internal static bool UseFP32 => CanUseFP32; + internal static void SuppressFp32() { } + internal static void RestoreFp32() { } +#endif + + public override int ArgCount => GetArgCount(UseFp32); + + private int GetArgCount(bool useFp32) + { + var count = 4; // key, element and either "FP32 {vector}" or VALUES {num}" + if (reducedDimensions.HasValue) count += 2; // [REDUCE {dim}] + + if (!useFp32) count += values.Length; // {vector} in the VALUES case + + if (useCheckAndSet) count++; // [CAS] + count += quantizationType switch + { + VectorQuantizationType.None or VectorQuantizationType.Binary => 1, // [NOQUANT] or [BIN] + VectorQuantizationType.Int8 => 0, // implicit + _ => throw new ArgumentOutOfRangeException(nameof(quantizationType)), + }; + + if (buildExplorationFactor.HasValue) count += 2; // [EF {build-exploration-factor}] + if (attributesJson is not null) count += 2; // [SETATTR {attributes}] + if (maxConnections.HasValue) count += 2; // [M {numlinks}] + return count; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + bool useFp32 = UseFp32; // snapshot to avoid race in debug scenarios + physical.WriteHeader(Command, GetArgCount(useFp32)); + physical.Write(key); + if (reducedDimensions.HasValue) + { + physical.WriteBulkString("REDUCE"u8); + physical.WriteBulkString(reducedDimensions.GetValueOrDefault()); + } + if (useFp32) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(values.Length); + foreach (var val in values.Span) + { + physical.WriteBulkString(val); + } + } + physical.WriteBulkString(element); + if (useCheckAndSet) physical.WriteBulkString("CAS"u8); + + switch (quantizationType) + { + case VectorQuantizationType.Int8: + break; + case VectorQuantizationType.None: + physical.WriteBulkString("NOQUANT"u8); + break; + case VectorQuantizationType.Binary: + physical.WriteBulkString("BIN"u8); + break; + default: + throw new ArgumentOutOfRangeException(nameof(quantizationType)); + } + if (buildExplorationFactor.HasValue) + { + physical.WriteBulkString("EF"u8); + physical.WriteBulkString(buildExplorationFactor.GetValueOrDefault()); + } + if (attributesJson is not null) + { + physical.WriteBulkString("SETATTR"u8); + physical.WriteBulkString(attributesJson); + } + if (maxConnections.HasValue) + { + physical.WriteBulkString("M"u8); + physical.WriteBulkString(maxConnections.GetValueOrDefault()); + } + } + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); +} diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs index d970168c2..73ed61c41 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -17,30 +17,42 @@ public KeyPrefixedVectorSetTests() prefixed = new KeyspaceIsolation.KeyPrefixedDatabase(mock, Encoding.UTF8.GetBytes("prefix:")); } + [Fact] + public void VectorSetAdd_Fp32() + { + if (BitConverter.IsLittleEndian) + { + Assert.True(VectorSetAddMessage.UseFp32); +#if DEBUG // can be suppressed + VectorSetAddMessage.SuppressFp32(); + Assert.False(VectorSetAddMessage.UseFp32); + VectorSetAddMessage.RestoreFp32(); + Assert.True(VectorSetAddMessage.UseFp32); +#endif + } + else + { + Assert.False(VectorSetAddMessage.UseFp32); + } + } + [Fact] public void VectorSetAdd_BasicCall() { - var vector = new float[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); prefixed.VectorSetAdd("vectorset", "element1", vector); mock.Received().VectorSetAdd( "prefix:vectorset", "element1", - vector, - null, - VectorQuantizationType.Int8, - null, - null, - false, - null, - CommandFlags.None); + vector); } [Fact] public void VectorSetAdd_WithAllParameters() { - var vector = new float[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); var attributes = """{"category":"test"}"""; prefixed.VectorSetAdd( @@ -71,78 +83,78 @@ public void VectorSetAdd_WithAllParameters() [Fact] public void VectorSetLength() { - prefixed.VectorSetLength("vectorset", CommandFlags.None); - mock.Received().VectorSetLength("prefix:vectorset", CommandFlags.None); + prefixed.VectorSetLength("vectorset"); + mock.Received().VectorSetLength("prefix:vectorset"); } [Fact] public void VectorSetDimension() { - prefixed.VectorSetDimension("vectorset", CommandFlags.None); - mock.Received().VectorSetDimension("prefix:vectorset", CommandFlags.None); + prefixed.VectorSetDimension("vectorset"); + mock.Received().VectorSetDimension("prefix:vectorset"); } [Fact] public void VectorSetGetApproximateVector() { - prefixed.VectorSetGetApproximateVector("vectorset", "member1", CommandFlags.None); - mock.Received().VectorSetGetApproximateVector("prefix:vectorset", "member1", CommandFlags.None); + prefixed.VectorSetGetApproximateVector("vectorset", "member1"); + mock.Received().VectorSetGetApproximateVector("prefix:vectorset", "member1"); } [Fact] public void VectorSetGetAttributesJson() { - prefixed.VectorSetGetAttributesJson("vectorset", "member1", CommandFlags.None); - mock.Received().VectorSetGetAttributesJson("prefix:vectorset", "member1", CommandFlags.None); + prefixed.VectorSetGetAttributesJson("vectorset", "member1"); + mock.Received().VectorSetGetAttributesJson("prefix:vectorset", "member1"); } [Fact] public void VectorSetInfo() { - prefixed.VectorSetInfo("vectorset", CommandFlags.None); - mock.Received().VectorSetInfo("prefix:vectorset", CommandFlags.None); + prefixed.VectorSetInfo("vectorset"); + mock.Received().VectorSetInfo("prefix:vectorset"); } [Fact] public void VectorSetContains() { - prefixed.VectorSetContains("vectorset", "member1", CommandFlags.None); - mock.Received().VectorSetContains("prefix:vectorset", "member1", CommandFlags.None); + prefixed.VectorSetContains("vectorset", "member1"); + mock.Received().VectorSetContains("prefix:vectorset", "member1"); } [Fact] public void VectorSetGetLinks() { - prefixed.VectorSetGetLinks("vectorset", "member1", CommandFlags.None); - mock.Received().VectorSetGetLinks("prefix:vectorset", "member1", CommandFlags.None); + prefixed.VectorSetGetLinks("vectorset", "member1"); + mock.Received().VectorSetGetLinks("prefix:vectorset", "member1"); } [Fact] public void VectorSetGetLinksWithScores() { - prefixed.VectorSetGetLinksWithScores("vectorset", "member1", CommandFlags.None); - mock.Received().VectorSetGetLinksWithScores("prefix:vectorset", "member1", CommandFlags.None); + prefixed.VectorSetGetLinksWithScores("vectorset", "member1"); + mock.Received().VectorSetGetLinksWithScores("prefix:vectorset", "member1"); } [Fact] public void VectorSetRandomMember() { - prefixed.VectorSetRandomMember("vectorset", CommandFlags.None); - mock.Received().VectorSetRandomMember("prefix:vectorset", CommandFlags.None); + prefixed.VectorSetRandomMember("vectorset"); + mock.Received().VectorSetRandomMember("prefix:vectorset"); } [Fact] public void VectorSetRandomMembers() { - prefixed.VectorSetRandomMembers("vectorset", 5, CommandFlags.None); - mock.Received().VectorSetRandomMembers("prefix:vectorset", 5, CommandFlags.None); + prefixed.VectorSetRandomMembers("vectorset", 5); + mock.Received().VectorSetRandomMembers("prefix:vectorset", 5); } [Fact] public void VectorSetRemove() { - prefixed.VectorSetRemove("vectorset", "member1", CommandFlags.None); - mock.Received().VectorSetRemove("prefix:vectorset", "member1", CommandFlags.None); + prefixed.VectorSetRemove("vectorset", "member1"); + mock.Received().VectorSetRemove("prefix:vectorset", "member1"); } [Fact] @@ -150,41 +162,21 @@ public void VectorSetSetAttributesJson() { var attributes = """{"category":"test"}"""; - prefixed.VectorSetSetAttributesJson("vectorset", "member1", attributes, CommandFlags.None); - mock.Received().VectorSetSetAttributesJson("prefix:vectorset", "member1", attributes, CommandFlags.None); + prefixed.VectorSetSetAttributesJson("vectorset", "member1", attributes); + mock.Received().VectorSetSetAttributesJson("prefix:vectorset", "member1", attributes); } [Fact] public void VectorSetSimilaritySearchByVector() { - var vector = new float[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); prefixed.VectorSetSimilaritySearchByVector( "vectorset", - vector, - 10, - false, - false, - null, - null, - null, - null, - false, - false, - CommandFlags.None); + vector); mock.Received().VectorSetSimilaritySearchByVector( "prefix:vectorset", - vector, - 10, - false, - false, - null, - null, - null, - null, - false, - false, - CommandFlags.None); + vector); } [Fact] @@ -221,23 +213,13 @@ public void VectorSetSimilaritySearchByMember() [Fact] public void VectorSetSimilaritySearchByVector_DefaultParameters() { - var vector = new float[] { 1.0f, 2.0f }.AsMemory(); + var vector = new[] { 1.0f, 2.0f }.AsMemory(); // Test that default parameters work correctly prefixed.VectorSetSimilaritySearchByVector("vectorset", vector); mock.Received().VectorSetSimilaritySearchByVector( "prefix:vectorset", - vector, - 10, - false, - false, - null, - null, - null, - null, - false, - false, - CommandFlags.None); + vector); } } } diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 6c9aff187..72fcbe136 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -9,8 +9,10 @@ public sealed class VectorSetIntegrationTests : TestBase { public VectorSetIntegrationTests(ITestOutputHelper output) : base(output) { } - [Fact] - public async Task VectorSetAdd_BasicOperation() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task VectorSetAdd_BasicOperation(bool suppressFp32) { using var conn = Create(); var db = conn.GetDatabase(); @@ -20,9 +22,18 @@ public async Task VectorSetAdd_BasicOperation() await db.KeyDeleteAsync(key); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; - var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); - Assert.True(result); + if (suppressFp32) VectorSetAddMessage.SuppressFp32(); + try + { + var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + Assert.True(result); + } + finally + { + if (suppressFp32) VectorSetAddMessage.RestoreFp32(); + } } [Fact] @@ -94,8 +105,10 @@ public async Task VectorSetDimension() Assert.Equal(5, dimension); } - [Fact] - public async Task VectorSetContains() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task VectorSetContains(bool suppressFp32) { using var conn = Create(); var db = conn.GetDatabase(); @@ -104,17 +117,27 @@ public async Task VectorSetContains() await db.KeyDeleteAsync(key); var vector = new float[] { 1.0f, 2.0f, 3.0f }; - await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + if (suppressFp32) VectorSetAddMessage.SuppressFp32(); + try + { + await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); - var exists = await db.VectorSetContainsAsync(key, "element1"); - var notExists = await db.VectorSetContainsAsync(key, "element2"); + var exists = await db.VectorSetContainsAsync(key, "element1"); + var notExists = await db.VectorSetContainsAsync(key, "element2"); - Assert.True(exists); - Assert.False(notExists); + Assert.True(exists); + Assert.False(notExists); + } + finally + { + if (suppressFp32) VectorSetAddMessage.RestoreFp32(); + } } - [Fact] - public async Task VectorSetGetApproximateVector() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task VectorSetGetApproximateVector(bool suppressFp32) { using var conn = Create(); var db = conn.GetDatabase(); @@ -123,19 +146,28 @@ public async Task VectorSetGetApproximateVector() await db.KeyDeleteAsync(key); var originalVector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; - await db.VectorSetAddAsync(key, "element1", originalVector.AsMemory()); + if (suppressFp32) VectorSetAddMessage.SuppressFp32(); + try + { + await db.VectorSetAddAsync(key, "element1", originalVector.AsMemory()); - using var retrievedLease = await db.VectorSetGetApproximateVectorAsync(key, "element1"); + using var retrievedLease = await db.VectorSetGetApproximateVectorAsync(key, "element1"); - Assert.NotNull(retrievedLease); - var retrievedVector = retrievedLease.Span; + Assert.NotNull(retrievedLease); + var retrievedVector = retrievedLease.Span; - Assert.Equal(originalVector.Length, retrievedVector.Length); - // Note: Due to quantization, values might not be exactly equal - for (int i = 0; i < originalVector.Length; i++) + Assert.Equal(originalVector.Length, retrievedVector.Length); + // Note: Due to quantization, values might not be exactly equal + for (int i = 0; i < originalVector.Length; i++) + { + Assert.True( + Math.Abs(originalVector[i] - retrievedVector[i]) < 0.1f, + $"Vector component {i} differs too much: expected {originalVector[i]}, got {retrievedVector[i]}"); + } + } + finally { - var delta = Math.Abs(originalVector[i] - retrievedVector[i]); - Assert.True(delta < 0.1f, $"Vector component {i} differs too much: expected {originalVector[i]}, got {retrievedVector[i]}"); + if (suppressFp32) VectorSetAddMessage.RestoreFp32(); } } @@ -152,7 +184,10 @@ public async Task VectorSetRemove() await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); var removed = await db.VectorSetRemoveAsync(key, "element1"); - Assert.Equal(1, removed); + Assert.True(removed); + + removed = await db.VectorSetRemoveAsync(key, "element1"); + Assert.False(removed); var exists = await db.VectorSetContainsAsync(key, "element1"); Assert.False(exists); From 9a4b9c3f1fff6c2c14214ad589156e2d358506a1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 11 Aug 2025 17:11:59 +0100 Subject: [PATCH 04/32] more tests --- .../VectorSetIntegrationTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 72fcbe136..e38f4d8d7 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -57,6 +57,39 @@ public async Task VectorSetAdd_WithAttributes() Assert.Equal(attributes, retrievedAttributes); } + [Theory] + [InlineData(VectorQuantizationType.Int8)] + [InlineData(VectorQuantizationType.None)] + [InlineData(VectorQuantizationType.Binary)] + public async Task VectorSetAdd_WithEverything(VectorQuantizationType quantizationType) + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var attributes = """{"category":"test","id":123}"""; + + var result = await db.VectorSetAddAsync( + key, + "element1", + vector.AsMemory(), + attributesJson: attributes, + useCheckAndSet: true, + quantizationType: quantizationType, + reducedDimensions: 64, + buildExplorationFactor: 300, + maxConnections: 32); + + Assert.True(result); + + // Verify attributes were stored + var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); + Assert.Equal(attributes, retrievedAttributes); + } + [Fact] public async Task VectorSetLength_EmptySet() { From ebce7ebc1bfad92ba1a1ad7efb1e6ac0bb279242 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 12 Aug 2025 10:12:02 +0100 Subject: [PATCH 05/32] ack experimental --- Directory.Build.props | 2 +- src/StackExchange.Redis/Experiments.cs | 41 +++++++ .../Interfaces/IDatabase.cs | 16 +++ .../Interfaces/IDatabaseAsync.cs | 16 +++ .../KeyspaceIsolation/KeyPrefixed.cs | 1 + .../PublicAPI/PublicAPI.Unshipped.txt | 110 +++++++++--------- src/StackExchange.Redis/RedisDatabase.cs | 6 +- 7 files changed, 134 insertions(+), 58 deletions(-) create mode 100644 src/StackExchange.Redis/Experiments.cs diff --git a/Directory.Build.props b/Directory.Build.props index 9f512d5e9..42de5875c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - NU5105;NU1507 + $(NoWarn);NU5105;NU1507;SER001 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes https://stackexchange.github.io/StackExchange.Redis/ MIT diff --git a/src/StackExchange.Redis/Experiments.cs b/src/StackExchange.Redis/Experiments.cs new file mode 100644 index 000000000..577c9f8c9 --- /dev/null +++ b/src/StackExchange.Redis/Experiments.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis +{ + // example usage: + // [Experimental(Experiments.SomeFeature, UrlFormat = Experiments.UrlFormat)] + // where SomeFeature has the next label, for example "SER042", and /docs/exp/SER042.md exists + internal static class Experiments + { + public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/"; + public const string VectorSets = "SER001"; + } +} + +#if !NET8_0_OR_GREATER +#pragma warning disable SA1403 +namespace System.Diagnostics.CodeAnalysis +#pragma warning restore SA1403 +{ + [AttributeUsage( + AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, + Inherited = false)] + internal sealed class ExperimentalAttribute(string diagnosticId) : Attribute + { + public string DiagnosticId { get; } = diagnosticId; + public string? UrlFormat { get; set; } + public string? Message { get; set; } + } +} +#endif diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 2021d1005..06fc82521 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Net; // ReSharper disable once CheckNamespace @@ -3447,6 +3448,7 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// if the element was added; if it already existed. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] bool VectorSetAdd( RedisKey key, RedisValue element, @@ -3466,6 +3468,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// The cardinality of the vectorset. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -3475,6 +3478,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// The dimension of vectors in the vectorset. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -3485,6 +3489,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// The vector as a pooled memory lease. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -3495,6 +3500,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// The attributes as a JSON string. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -3504,6 +3510,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// Information about the vectorset. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -3514,6 +3521,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// True if the member exists, false otherwise. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -3524,6 +3532,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// The linked members. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -3534,6 +3543,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// The linked members with their similarity scores. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -3543,6 +3553,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// A random member from the vectorset, or null if the vectorset is empty. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -3553,6 +3564,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// Random members from the vectorset. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -3563,6 +3575,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// if the member was removed; if it was not found. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -3574,6 +3587,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// True if successful. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); /// @@ -3593,6 +3607,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// Similar vectors with their similarity scores. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Lease? VectorSetSimilaritySearchByVector( RedisKey key, ReadOnlyMemory vector, @@ -3624,6 +3639,7 @@ bool VectorSetAdd( /// The flags to use for this operation. /// Similar vectors with their similarity scores. /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Lease? VectorSetSimilaritySearchByMember( RedisKey key, RedisValue member, diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 3c11e2e79..b4a8105a6 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading.Tasks; @@ -848,6 +849,7 @@ IAsyncEnumerable SortedSetScanAsync( // Vector Set operations /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetAddAsync( RedisKey key, RedisValue element, @@ -861,42 +863,55 @@ Task VectorSetAddAsync( CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task?> VectorSetSimilaritySearchByVectorAsync( RedisKey key, ReadOnlyMemory vector, @@ -912,6 +927,7 @@ Task VectorSetAddAsync( CommandFlags flags = CommandFlags.None); /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task?> VectorSetSimilaritySearchByMemberAsync( RedisKey key, RedisValue member, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index f640a0b68..0b727b00b 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -815,6 +815,7 @@ public void WaitAll(params Task[] tasks) => Inner.WaitAll(tasks); // Vector Set operations - async methods + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] public Task VectorSetAddAsync( RedisKey key, RedisValue element, diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 3d583e69e..c394b8e38 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,56 +1,56 @@ #nullable enable -StackExchange.Redis.VectorQuantizationType -StackExchange.Redis.VectorQuantizationType.Binary = 3 -> StackExchange.Redis.VectorQuantizationType -StackExchange.Redis.VectorQuantizationType.Int8 = 2 -> StackExchange.Redis.VectorQuantizationType -StackExchange.Redis.VectorQuantizationType.None = 1 -> StackExchange.Redis.VectorQuantizationType -StackExchange.Redis.VectorQuantizationType.Unknown = 0 -> StackExchange.Redis.VectorQuantizationType -StackExchange.Redis.VectorSetInfo -StackExchange.Redis.VectorSetInfo.Dimension.get -> int -StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long -StackExchange.Redis.VectorSetInfo.Length.get -> long -StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int -StackExchange.Redis.VectorSetInfo.QuantizationType.get -> StackExchange.Redis.VectorQuantizationType -StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void -StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorQuantizationType quantizationType, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void -StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long -StackExchange.Redis.VectorSetLink -StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue -StackExchange.Redis.VectorSetLink.Score.get -> double -StackExchange.Redis.VectorSetLink.VectorSetLink() -> void -StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void -StackExchange.Redis.VectorSimilarityResult -StackExchange.Redis.VectorSimilarityResult.AttributesJson.get -> string? -StackExchange.Redis.VectorSimilarityResult.Member.get -> StackExchange.Redis.RedisValue -StackExchange.Redis.VectorSimilarityResult.Score.get -> double -StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult() -> void -StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void -StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int -StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -StackExchange.Redis.IDatabase.VectorSetGetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? -StackExchange.Redis.IDatabase.VectorSetGetLinks(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -StackExchange.Redis.IDatabase.VectorSetGetLinksWithScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.VectorSetInfo? -StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! -StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -StackExchange.Redis.IDatabaseAsync.VectorSetGetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksWithScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int +[SER001]StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinks(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinksWithScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.VectorSetInfo? +[SER001]StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +[SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksWithScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorQuantizationType.Binary = 3 -> StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorQuantizationType.Int8 = 2 -> StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorQuantizationType.None = 1 -> StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorQuantizationType.Unknown = 0 -> StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorSetInfo +[SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int +[SER001]StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long +[SER001]StackExchange.Redis.VectorSetInfo.Length.get -> long +[SER001]StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int +[SER001]StackExchange.Redis.VectorSetInfo.QuantizationType.get -> StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorQuantizationType quantizationType, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long +[SER001]StackExchange.Redis.VectorSetLink +[SER001]StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetLink.Score.get -> double +[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink() -> void +[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void +[SER001]StackExchange.Redis.VectorSimilarityResult +[SER001]StackExchange.Redis.VectorSimilarityResult.AttributesJson.get -> string? +[SER001]StackExchange.Redis.VectorSimilarityResult.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSimilarityResult.Score.get -> double +[SER001]StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult() -> void +[SER001]StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 82c1b7537..40a036520 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5776,7 +5776,8 @@ public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flag public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); + return ExecuteSync(msg, ResultProcessor.RedisValueLease); } public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -5896,7 +5897,8 @@ public Task VectorSetContainsAsync(RedisKey key, RedisValue member, Comman public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); + return ExecuteAsync(msg, ResultProcessor.RedisValueLease); } public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) From 579133b560c8936847571795af9c44f7c1ff7546 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 12 Aug 2025 10:12:21 +0100 Subject: [PATCH 06/32] ack experimental --- docs/exp/SER001.md | 22 ++++ src/StackExchange.Redis/ResultProcessor.cs | 101 +++++++++++------- .../VectorQuantizationType.cs | 3 + .../VectorSetAddMessage.cs | 1 - src/StackExchange.Redis/VectorSetInfo.cs | 65 ++++++----- src/StackExchange.Redis/VectorSetLink.cs | 5 +- .../VectorSimilarityResult.cs | 5 +- 7 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 docs/exp/SER001.md diff --git a/docs/exp/SER001.md b/docs/exp/SER001.md new file mode 100644 index 000000000..2def8be6e --- /dev/null +++ b/docs/exp/SER001.md @@ -0,0 +1,22 @@ +At the current time, [Redis documents that](https://redis.io/docs/latest/commands/vadd/): + +> Vector set is a new data type that is currently in preview and may be subject to change. + +As such, the corresponding library feature must also be considered subject to change: + +1. Existing bindings may cease working correctly if the underlying server API changes. +2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time + or run-time breaks. + +While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress +this warning by adding the following to your `csproj` file: + +```xml +$(NoWarn);SER001 +``` + +or more granularly / locally in C#: + +``` c# +#pragma warning disable SER001 +``` \ No newline at end of file diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index f326fe56e..ef970be13 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -102,6 +102,8 @@ public static readonly ResultProcessor> public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); + public static readonly ResultProcessor?> + RedisValueLease = new RedisValueLeaseProcessor(); public static readonly ResultProcessor Int64Array = new Int64ArrayProcessor(); @@ -1707,6 +1709,64 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private abstract class LeaseProcessor : ResultProcessor?> + { + protected sealed override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + + // deal with null + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + + // lease and fill + var items = result.GetItems(); + var length = checked((int)items.Length); + var lease = Lease.Create(length, clear: false); // note this handles zero nicely + var target = lease.Span; + int index = 0; + foreach (ref RawResult item in items) + { + if (!TryParse(item, out target[index++])) + { + // something went wrong; recycle and quit + lease.Dispose(); + return false; + } + } + Debug.Assert(index == length, "length mismatch"); + SetResult(message, lease); + return true; + } + + protected abstract bool TryParse(in RawResult raw, out T parsed); + } + + private sealed class RedisValueLeaseProcessor : LeaseProcessor + { + protected override bool TryParse(in RawResult raw, out RedisValue parsed) + { + parsed = raw.AsRedisValue(); + return true; + } + } + + private sealed class LeaseFloat32Processor : LeaseProcessor + { + protected override bool TryParse(in RawResult raw, out float parsed) + { + var result = raw.TryGetDouble(out double val); + parsed = (float)val; + return result; + } + } + private sealed class Int64ArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -2116,47 +2176,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class LeaseFloat32Processor : ResultProcessor?> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - switch (result.Resp2TypeArray) - { - case ResultType.Array: - if (result.IsNull) - { - SetResult(message, null); - return true; - } - - var items = result.GetItems(); - if (items.IsEmpty) - { - SetResult(message, Lease.Empty); - return true; - } - - var length = checked((int)items.Length); - var lease = Lease.Create(length, clear: false); - var target = lease.Span; - int index = 0; - foreach (ref RawResult item in items) - { - if (!item.TryGetDouble(out double val)) break; - target[index++] = (float)val; - } - if (index == length) - { - SetResult(message, lease); - return true; - } - lease.Dispose(); // something went wrong; recycle - break; - } - return false; - } - } - private sealed class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) diff --git a/src/StackExchange.Redis/VectorQuantizationType.cs b/src/StackExchange.Redis/VectorQuantizationType.cs index 666898be8..66a210714 100644 --- a/src/StackExchange.Redis/VectorQuantizationType.cs +++ b/src/StackExchange.Redis/VectorQuantizationType.cs @@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace StackExchange.Redis; /// /// Specifies the quantization type for vectors in a vectorset. /// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] public enum VectorQuantizationType { /// diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs index 847b507a5..d41f3df53 100644 --- a/src/StackExchange.Redis/VectorSetAddMessage.cs +++ b/src/StackExchange.Redis/VectorSetAddMessage.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; diff --git a/src/StackExchange.Redis/VectorSetInfo.cs b/src/StackExchange.Redis/VectorSetInfo.cs index 78a2557a8..a755ae576 100644 --- a/src/StackExchange.Redis/VectorSetInfo.cs +++ b/src/StackExchange.Redis/VectorSetInfo.cs @@ -1,40 +1,47 @@ using System; +using System.Diagnostics.CodeAnalysis; -namespace StackExchange.Redis +namespace StackExchange.Redis; + +/// +/// Contains metadata information about a vectorset returned by VINFO command. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public readonly struct VectorSetInfo( + VectorQuantizationType quantizationType, + int dimension, + long length, + int maxLevel, + long vectorSetUid, + long hnswMaxNodeUid) { /// - /// Contains metadata information about a vectorset returned by VINFO command. + /// The quantization type used for vectors in this vectorset. /// - public readonly struct VectorSetInfo(VectorQuantizationType quantizationType, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) - { - /// - /// The quantization type used for vectors in this vectorset. - /// - public VectorQuantizationType QuantizationType { get; } = quantizationType; + public VectorQuantizationType QuantizationType { get; } = quantizationType; - /// - /// The number of dimensions in each vector. - /// - public int Dimension { get; } = dimension; + /// + /// The number of dimensions in each vector. + /// + public int Dimension { get; } = dimension; - /// - /// The number of elements (cardinality) in the vectorset. - /// - public long Length { get; } = length; + /// + /// The number of elements (cardinality) in the vectorset. + /// + public long Length { get; } = length; - /// - /// The maximum level in the HNSW graph structure. - /// - public int MaxLevel { get; } = maxLevel; + /// + /// The maximum level in the HNSW graph structure. + /// + public int MaxLevel { get; } = maxLevel; - /// - /// The unique identifier for this vectorset. - /// - public long VectorSetUid { get; } = vectorSetUid; + /// + /// The unique identifier for this vectorset. + /// + public long VectorSetUid { get; } = vectorSetUid; - /// - /// The maximum node unique identifier in the HNSW graph. - /// - public long HnswMaxNodeUid { get; } = hnswMaxNodeUid; - } + /// + /// The maximum node unique identifier in the HNSW graph. + /// + public long HnswMaxNodeUid { get; } = hnswMaxNodeUid; } diff --git a/src/StackExchange.Redis/VectorSetLink.cs b/src/StackExchange.Redis/VectorSetLink.cs index b2256c10d..3f39b9472 100644 --- a/src/StackExchange.Redis/VectorSetLink.cs +++ b/src/StackExchange.Redis/VectorSetLink.cs @@ -1,9 +1,12 @@ -namespace StackExchange.Redis; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; /// /// Represents a link/connection between members in a vectorset with similarity score. /// Used by VLINKS command with WITHSCORES option. /// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] public readonly struct VectorSetLink(RedisValue member, double score) { /// diff --git a/src/StackExchange.Redis/VectorSimilarityResult.cs b/src/StackExchange.Redis/VectorSimilarityResult.cs index 0a8a8826a..55c961d7d 100644 --- a/src/StackExchange.Redis/VectorSimilarityResult.cs +++ b/src/StackExchange.Redis/VectorSimilarityResult.cs @@ -1,8 +1,11 @@ -namespace StackExchange.Redis; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; /// /// Represents a result from vector similarity search operations. /// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] public readonly struct VectorSimilarityResult(RedisValue member, double score = double.NaN, string? attributesJson = null) { /// From f20db6730a8c42075c9c7fc53af727e1c61eb75f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 12 Aug 2025 10:28:17 +0100 Subject: [PATCH 07/32] links --- src/StackExchange.Redis/RedisDatabase.cs | 6 ++- src/StackExchange.Redis/ResultProcessor.cs | 57 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 40a036520..7e9ab3878 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5782,7 +5782,8 @@ public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flag public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); + return ExecuteSync(msg, ResultProcessor.LeaseVectorSetLink); } public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -5903,7 +5904,8 @@ public Task VectorSetContainsAsync(RedisKey key, RedisValue member, Comman public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); + return ExecuteAsync(msg, ResultProcessor.LeaseVectorSetLink); } public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index ef970be13..3e25322dc 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -63,6 +63,8 @@ public static readonly ResultProcessor public static readonly ResultProcessor Int32 = new Int32Processor(); public static readonly ResultProcessor?> LeaseFloat32 = new LeaseFloat32Processor(); + public static readonly ResultProcessor?> LeaseVectorSetLink = new LeaseVectorSetLinkProcessor(); + public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -1748,6 +1750,50 @@ protected sealed override bool SetResultCore(PhysicalConnection connection, Mess protected abstract bool TryParse(in RawResult raw, out T parsed); } + private abstract class InterleavedLeaseProcessor : ResultProcessor?> + { + protected sealed override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + + // deal with null + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + + // lease and fill + var items = result.GetItems(); + var length = checked((int)items.Length) / 2; + var lease = Lease.Create(length, clear: false); // note this handles zero nicely + var target = lease.Span; + + var iter = items.GetEnumerator(); + for (int i = 0; i < target.Length; i++) + { + bool ok = iter.MoveNext(); + if (ok) + { + ref readonly RawResult first = ref iter.Current; + ok = iter.MoveNext() && TryParse(in first, in iter.Current, out target[i]); + } + if (!ok) + { + lease.Dispose(); + return false; + } + } + SetResult(message, lease); + return true; + } + + protected abstract bool TryParse(in RawResult first, in RawResult second, out T parsed); + } + private sealed class RedisValueLeaseProcessor : LeaseProcessor { protected override bool TryParse(in RawResult raw, out RedisValue parsed) @@ -1767,6 +1813,17 @@ protected override bool TryParse(in RawResult raw, out float parsed) } } + private sealed class LeaseVectorSetLinkProcessor : InterleavedLeaseProcessor + { + protected override bool TryParse(in RawResult first, in RawResult second, out VectorSetLink parsed) + { + var member = first.AsRedisValue(); + bool result = second.TryGetDouble(out var score); + parsed = new VectorSetLink(member, score); + return result; + } + } + private sealed class Int64ArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) From 4b4225ea175f9cfdd27a531bcafae52fb71126da Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 12 Aug 2025 14:04:34 +0100 Subject: [PATCH 08/32] VLINKS impl --- .../PublicAPI/PublicAPI.Unshipped.txt | 1 + src/StackExchange.Redis/RedisDatabase.cs | 8 +- src/StackExchange.Redis/ResultProcessor.cs | 119 ++++++++++++--- src/StackExchange.Redis/VectorSetLink.cs | 3 + .../VectorSetIntegrationTests.cs | 138 +++++++++++++++--- 5 files changed, 228 insertions(+), 41 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index c394b8e38..c3cc067ac 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +[SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! [SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7e9ab3878..b7ec1b876 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5777,13 +5777,13 @@ public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flag public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); - return ExecuteSync(msg, ResultProcessor.RedisValueLease); + return ExecuteSync(msg, ResultProcessor.VectorSetLinks); } public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); - return ExecuteSync(msg, ResultProcessor.LeaseVectorSetLink); + return ExecuteSync(msg, ResultProcessor.VectorSetLinksWithScores); } public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -5899,13 +5899,13 @@ public Task VectorSetContainsAsync(RedisKey key, RedisValue member, Comman public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); - return ExecuteAsync(msg, ResultProcessor.RedisValueLease); + return ExecuteAsync(msg, ResultProcessor.VectorSetLinks); } public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); - return ExecuteAsync(msg, ResultProcessor.LeaseVectorSetLink); + return ExecuteAsync(msg, ResultProcessor.VectorSetLinksWithScores); } public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 3e25322dc..02964ce02 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -63,7 +63,8 @@ public static readonly ResultProcessor public static readonly ResultProcessor Int32 = new Int32Processor(); public static readonly ResultProcessor?> LeaseFloat32 = new LeaseFloat32Processor(); - public static readonly ResultProcessor?> LeaseVectorSetLink = new LeaseVectorSetLinkProcessor(); + public static readonly ResultProcessor?> VectorSetLinksWithScores = new VectorSetLinksWithScoresProcessor(); + public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -104,8 +105,6 @@ public static readonly ResultProcessor> public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); - public static readonly ResultProcessor?> - RedisValueLease = new RedisValueLeaseProcessor(); public static readonly ResultProcessor Int64Array = new Int64ArrayProcessor(); @@ -1713,7 +1712,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes private abstract class LeaseProcessor : ResultProcessor?> { - protected sealed override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { if (result.Resp2TypeArray != ResultType.Array) { @@ -1752,7 +1751,7 @@ protected sealed override bool SetResultCore(PhysicalConnection connection, Mess private abstract class InterleavedLeaseProcessor : ResultProcessor?> { - protected sealed override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { if (result.Resp2TypeArray != ResultType.Array) { @@ -1794,32 +1793,114 @@ protected sealed override bool SetResultCore(PhysicalConnection connection, Mess protected abstract bool TryParse(in RawResult first, in RawResult second, out T parsed); } - private sealed class RedisValueLeaseProcessor : LeaseProcessor + // takes a nested vector of the form [[A],[B,C],[D]] and exposes it as [A,B,C,D]; this is + // especially useful for VLINKS + private abstract class FlattenedLeaseProcessor : ResultProcessor?> { - protected override bool TryParse(in RawResult raw, out RedisValue parsed) + protected virtual long GetArrayLength(in RawResult array) => array.GetItems().Length; + + protected virtual bool TryReadOne(ref Sequence.Enumerator reader, out T value) { - parsed = raw.AsRedisValue(); - return true; + if (reader.MoveNext() && TryReadOne(reader.Current, out value)) + { + return true; + } + value = default!; + return false; + } + + protected virtual bool TryReadOne(in RawResult result, out T value) + { + value = default!; + return false; + } + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + var items = result.GetItems(); + long length = 0; + foreach (ref RawResult item in items) + { + if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + { + length += GetArrayLength(item); + } + } + + if (length == 0) + { + SetResult(message, Lease.Empty); + return true; + } + var lease = Lease.Create(checked((int)length), clear: false); + int index = 0; + var target = lease.Span; + foreach (ref RawResult item in items) + { + if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + { + var iter = item.GetItems().GetEnumerator(); + while (index < target.Length && TryReadOne(ref iter, out T value)) + { + target[index++] = value; + } + } + } + + if (index == length) + { + SetResult(message, lease); + return true; + } + lease.Dispose(); // failed to fill? + return false; } } - private sealed class LeaseFloat32Processor : LeaseProcessor + private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor { - protected override bool TryParse(in RawResult raw, out float parsed) + protected override long GetArrayLength(in RawResult array) => array.GetItems().Length / 2; + + protected override bool TryReadOne(ref Sequence.Enumerator reader, out VectorSetLink value) { - var result = raw.TryGetDouble(out double val); - parsed = (float)val; - return result; + if (reader.MoveNext()) + { + ref readonly RawResult first = ref reader.Current; + if (reader.MoveNext() && reader.Current.TryGetDouble(out var score)) + { + value = new VectorSetLink(first.AsRedisValue(), score); + return true; + } + } + value = default; + return false; + } + } + + private sealed class VectorSetLinksProcessor : FlattenedLeaseProcessor + { + protected override bool TryReadOne(in RawResult result, out RedisValue value) + { + value = result.AsRedisValue(); + return true; } } - private sealed class LeaseVectorSetLinkProcessor : InterleavedLeaseProcessor + private sealed class LeaseFloat32Processor : LeaseProcessor { - protected override bool TryParse(in RawResult first, in RawResult second, out VectorSetLink parsed) + protected override bool TryParse(in RawResult raw, out float parsed) { - var member = first.AsRedisValue(); - bool result = second.TryGetDouble(out var score); - parsed = new VectorSetLink(member, score); + var result = raw.TryGetDouble(out double val); + parsed = (float)val; return result; } } diff --git a/src/StackExchange.Redis/VectorSetLink.cs b/src/StackExchange.Redis/VectorSetLink.cs index 3f39b9472..c18e8a95f 100644 --- a/src/StackExchange.Redis/VectorSetLink.cs +++ b/src/StackExchange.Redis/VectorSetLink.cs @@ -18,4 +18,7 @@ public readonly struct VectorSetLink(RedisValue member, double score) /// The similarity score between the queried member and this linked member. /// public double Score { get; } = score; + + /// + public override string ToString() => $"{Member}: {Score}"; } diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index e38f4d8d7..2df8605d4 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -1,20 +1,19 @@ using System; +using System.Linq; using System.Threading.Tasks; using Xunit; namespace StackExchange.Redis.Tests; [RunPerProtocol] -public sealed class VectorSetIntegrationTests : TestBase +public sealed class VectorSetIntegrationTests(ITestOutputHelper output) : TestBase(output) { - public VectorSetIntegrationTests(ITestOutputHelper output) : base(output) { } - [Theory] [InlineData(true)] [InlineData(false)] public async Task VectorSetAdd_BasicOperation(bool suppressFp32) { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -39,7 +38,7 @@ public async Task VectorSetAdd_BasicOperation(bool suppressFp32) [Fact] public async Task VectorSetAdd_WithAttributes() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -63,7 +62,7 @@ public async Task VectorSetAdd_WithAttributes() [InlineData(VectorQuantizationType.Binary)] public async Task VectorSetAdd_WithEverything(VectorQuantizationType quantizationType) { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -93,7 +92,7 @@ public async Task VectorSetAdd_WithEverything(VectorQuantizationType quantizatio [Fact] public async Task VectorSetLength_EmptySet() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -106,7 +105,7 @@ public async Task VectorSetLength_EmptySet() [Fact] public async Task VectorSetLength_WithElements() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -125,7 +124,7 @@ public async Task VectorSetLength_WithElements() [Fact] public async Task VectorSetDimension() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -143,7 +142,7 @@ public async Task VectorSetDimension() [InlineData(false)] public async Task VectorSetContains(bool suppressFp32) { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -172,7 +171,7 @@ public async Task VectorSetContains(bool suppressFp32) [InlineData(false)] public async Task VectorSetGetApproximateVector(bool suppressFp32) { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -207,7 +206,7 @@ public async Task VectorSetGetApproximateVector(bool suppressFp32) [Fact] public async Task VectorSetRemove() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -229,7 +228,7 @@ public async Task VectorSetRemove() [Fact] public async Task VectorSetInfo() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -249,7 +248,7 @@ public async Task VectorSetInfo() [Fact] public async Task VectorSetRandomMember() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -268,7 +267,7 @@ public async Task VectorSetRandomMember() [Fact] public async Task VectorSetRandomMembers() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -292,7 +291,7 @@ public async Task VectorSetRandomMembers() [Fact] public async Task VectorSetSimilaritySearch_WithVector() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -324,7 +323,7 @@ public async Task VectorSetSimilaritySearch_WithVector() [Fact] public async Task VectorSetSimilaritySearch_WithMember() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -350,7 +349,7 @@ public async Task VectorSetSimilaritySearch_WithMember() [Fact] public async Task VectorSetSimilaritySearch_WithAttributes() { - using var conn = Create(); + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); var key = Me(); @@ -375,4 +374,107 @@ public async Task VectorSetSimilaritySearch_WithAttributes() Assert.False(double.IsNaN(result.Score)); Assert.Equal(attributes, result.AttributesJson); } + + [Fact] + public async Task VectorSetSetAttributesJson() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + var vector = new float[] { 1.0f, 2.0f, 3.0f }; + await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + + // Set attributes for existing element + var attributes = """{"category":"updated","priority":"high","timestamp":"2024-01-01"}"""; + var result = await db.VectorSetSetAttributesJsonAsync(key, "element1", attributes); + + Assert.True(result); + + // Verify attributes were set + var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); + Assert.Equal(attributes, retrievedAttributes); + + // Try setting attributes for non-existent element + var failResult = await db.VectorSetSetAttributesJsonAsync(key, "nonexistent", attributes); + Assert.False(failResult); + } + + [Fact] + public async Task VectorSetGetLinks() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + // Add some vectors that should be linked + var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector3 = new float[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + + // Get links for element1 (should include similar vectors) + using var links = await db.VectorSetGetLinksAsync(key, "element1"); + + Assert.NotNull(links); + var linksArray = links.Span.ToArray(); + + // Should contain the other elements + Assert.Equal(2, linksArray.Length); + Assert.Contains("element2", linksArray); + Assert.Contains("element3", linksArray); + } + + [Fact] + public async Task VectorSetGetLinksWithScores() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + // Add some vectors with known relationships + var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector3 = new float[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 + + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + + // Get links with scores for element1 + using var linksWithScores = await db.VectorSetGetLinksWithScoresAsync(key, "element1"); + Assert.NotNull(linksWithScores); + foreach (var link in linksWithScores.Span) + { + Log(link.ToString()); + } + + var linksArray = linksWithScores.Span.ToArray(); + Assert.NotEmpty(linksArray); + + // Verify each link has a valid score + Assert.All(linksArray, link => + { + Assert.False(link.Member.IsNull); + Assert.False(double.IsNaN(link.Score)); + Assert.True(link.Score >= 0.0); // Similarity scores should be non-negative + }); + + // Should contain the other elements + Assert.Equal(2, linksArray.Length); + Assert.Contains(linksArray, l => l.Member == "element2"); + Assert.Contains(linksArray, l => l.Member == "element3"); + + Assert.True(linksArray.Single(l => l.Member == "element2").Score > 0.9); // similar + Assert.True(linksArray.Single(l => l.Member == "element3").Score < 0.8); // less-so + } } From e0b959ddc710cf892b0c7a20ae8c9eb1ec558506 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 12 Aug 2025 15:05:58 +0100 Subject: [PATCH 09/32] implement VSIM message --- .../Interfaces/IDatabase.cs | 8 +- .../Interfaces/IDatabaseAsync.cs | 12 +- .../KeyspaceIsolation/KeyPrefixed.cs | 8 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 8 +- .../PublicAPI/PublicAPI.Unshipped.txt | 20 +- src/StackExchange.Redis/RedisDatabase.cs | 99 +++++-- src/StackExchange.Redis/ResultProcessor.cs | 37 ++- .../VectorSetSimilaritySearchMessage.cs | 245 ++++++++++++++++++ ....cs => VectorSetSimilaritySearchResult.cs} | 2 +- 9 files changed, 380 insertions(+), 59 deletions(-) create mode 100644 src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs rename src/StackExchange.Redis/{VectorSimilarityResult.cs => VectorSetSimilaritySearchResult.cs} (85%) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 06fc82521..209bc7960 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3608,10 +3608,10 @@ bool VectorSetAdd( /// Similar vectors with their similarity scores. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetSimilaritySearchByVector( + Lease? VectorSetSimilaritySearchByVector( RedisKey key, ReadOnlyMemory vector, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -3640,10 +3640,10 @@ bool VectorSetAdd( /// Similar vectors with their similarity scores. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetSimilaritySearchByMember( + Lease? VectorSetSimilaritySearchByMember( RedisKey key, RedisValue member, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b4a8105a6..3436c73a7 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -910,12 +910,12 @@ Task VectorSetAddAsync( [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); - /// + /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetSimilaritySearchByVectorAsync( + Task?> VectorSetSimilaritySearchByVectorAsync( RedisKey key, ReadOnlyMemory vector, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -926,12 +926,12 @@ Task VectorSetAddAsync( bool disableThreading = false, CommandFlags flags = CommandFlags.None); - /// + /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetSimilaritySearchByMemberAsync( + Task?> VectorSetSimilaritySearchByMemberAsync( RedisKey key, RedisValue member, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 0b727b00b..cd060cbd5 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -865,10 +865,10 @@ public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandF public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, jsonAttributes, flags); - public Task?> VectorSetSimilaritySearchByVectorAsync( + public Task?> VectorSetSimilaritySearchByVectorAsync( RedisKey key, ReadOnlyMemory vector, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -880,10 +880,10 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe CommandFlags flags = CommandFlags.None) => Inner.VectorSetSimilaritySearchByVectorAsync(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - public Task?> VectorSetSimilaritySearchByMemberAsync( + public Task?> VectorSetSimilaritySearchByMemberAsync( RedisKey key, RedisValue member, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 164b61457..a8c42d20f 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -855,10 +855,10 @@ public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => Inner.VectorSetSetAttributesJson(ToInner(key), member, jsonAttributes, flags); - public Lease? VectorSetSimilaritySearchByVector( + public Lease? VectorSetSimilaritySearchByVector( RedisKey key, ReadOnlyMemory vector, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -870,10 +870,10 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j CommandFlags flags = CommandFlags.None) => Inner.VectorSetSimilaritySearchByVector(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - public Lease? VectorSetSimilaritySearchByMember( + public Lease? VectorSetSimilaritySearchByMember( RedisKey key, RedisValue member, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index c3cc067ac..ec9fd22d3 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -13,8 +13,8 @@ [SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! [SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -28,8 +28,8 @@ [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, long count = 10, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! [SER001]StackExchange.Redis.VectorQuantizationType [SER001]StackExchange.Redis.VectorQuantizationType.Binary = 3 -> StackExchange.Redis.VectorQuantizationType [SER001]StackExchange.Redis.VectorQuantizationType.Int8 = 2 -> StackExchange.Redis.VectorQuantizationType @@ -49,9 +49,9 @@ [SER001]StackExchange.Redis.VectorSetLink.Score.get -> double [SER001]StackExchange.Redis.VectorSetLink.VectorSetLink() -> void [SER001]StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void -[SER001]StackExchange.Redis.VectorSimilarityResult -[SER001]StackExchange.Redis.VectorSimilarityResult.AttributesJson.get -> string? -[SER001]StackExchange.Redis.VectorSimilarityResult.Member.get -> StackExchange.Redis.RedisValue -[SER001]StackExchange.Redis.VectorSimilarityResult.Score.get -> double -[SER001]StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult() -> void -[SER001]StackExchange.Redis.VectorSimilarityResult.VectorSimilarityResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void \ No newline at end of file +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.AttributesJson.get -> string? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Score.get -> double +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult() -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index b7ec1b876..c44e5b71a 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5722,7 +5722,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - // Vector Set operations - stub implementations (to be implemented) public bool VectorSetAdd( RedisKey key, RedisValue element, @@ -5765,7 +5764,8 @@ public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.No public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); + return ExecuteSync(msg, ResultProcessor.VectorSetInfo); } public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -5810,10 +5810,10 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j return ExecuteSync(msg, ResultProcessor.Boolean); } - public Lease? VectorSetSimilaritySearchByVector( + public Lease? VectorSetSimilaritySearchByVector( RedisKey key, ReadOnlyMemory vector, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -5824,13 +5824,28 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j bool disableThreading = false, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); - } - - public Lease? VectorSetSimilaritySearchByMember( + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + RedisValue.Null, + vector, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteSync(msg, ResultProcessor.LeaseVectorSimilarityResult); + } + + public Lease? VectorSetSimilaritySearchByMember( RedisKey key, RedisValue member, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -5841,7 +5856,22 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j bool disableThreading = false, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + member, + ReadOnlyMemory.Empty, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteSync(msg, ResultProcessor.LeaseVectorSimilarityResult); } // Vector Set async operations @@ -5887,7 +5917,8 @@ public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = Comm public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); + return ExecuteAsync(msg, ResultProcessor.VectorSetInfo); } public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -5932,10 +5963,10 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe return ExecuteAsync(msg, ResultProcessor.Boolean); } - public Task?> VectorSetSimilaritySearchByVectorAsync( + public Task?> VectorSetSimilaritySearchByVectorAsync( RedisKey key, ReadOnlyMemory vector, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -5946,13 +5977,28 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe bool disableThreading = false, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); - } - - public Task?> VectorSetSimilaritySearchByMemberAsync( + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + RedisValue.Null, + vector, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteAsync(msg, ResultProcessor.LeaseVectorSimilarityResult); + } + + public Task?> VectorSetSimilaritySearchByMemberAsync( RedisKey key, RedisValue member, - long count = 10, + int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, @@ -5963,7 +6009,22 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe bool disableThreading = false, CommandFlags flags = CommandFlags.None) { - throw new NotImplementedException("Vector Set operations are not yet implemented"); + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + member, + ReadOnlyMemory.Empty, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteAsync(msg, ResultProcessor.LeaseVectorSimilarityResult); } } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 02964ce02..238041492 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -65,6 +65,9 @@ public static readonly ResultProcessor public static readonly ResultProcessor?> VectorSetLinksWithScores = new VectorSetLinksWithScoresProcessor(); public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); + public static readonly ResultProcessor?> LeaseVectorSimilarityResult = new VectorSetSimilaritySearchProcessor(); + + public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -706,7 +709,7 @@ public bool TryParse(in RawResult result, out T[]? pairs) count = (int)arr.Length; if (count == 0) { - return Array.Empty(); + return []; } bool interleaved = !(result.IsResp3 && AllowJaggedPairs && IsAllJaggedPairs(arr)); @@ -1895,6 +1898,18 @@ protected override bool TryReadOne(in RawResult result, out RedisValue value) } } + private sealed class VectorSetInfoProcessor : ResultProcessor + { + protected override bool + SetResultCore(PhysicalConnection connection, Message message, in RawResult result) => false; + } + + private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> + { + protected override bool + SetResultCore(PhysicalConnection connection, Message message, in RawResult result) => false; + } + private sealed class LeaseFloat32Processor : LeaseProcessor { protected override bool TryParse(in RawResult raw, out float parsed) @@ -2357,7 +2372,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (result.IsNull) { // Server returns 'nil' if no entries are returned for the given stream. - SetResult(message, Array.Empty()); + SetResult(message, []); return true; } @@ -2462,7 +2477,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (result.IsNull) { // Nothing returned for any of the requested streams. The server returns 'nil'. - SetResult(message, Array.Empty()); + SetResult(message, []); return true; } @@ -2528,7 +2543,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var entries = ParseRedisStreamEntries(items[1]); // [2] The array of message IDs deleted from the stream that were in the PEL. // This is not available in 6.2 so we need to be defensive when reading this part of the response. - var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? Array.Empty(); + var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? []; SetResult(message, new StreamAutoClaimResult(nextStartId, entries, deletedIds)); return true; @@ -2554,10 +2569,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // [0] The next start ID. var nextStartId = items[0].AsRedisValue(); // [1] The array of claimed message IDs. - var claimedIds = items[1].GetItemsAsValues() ?? Array.Empty(); + var claimedIds = items[1].GetItemsAsValues() ?? []; // [2] The array of message IDs deleted from the stream that were in the PEL. // This is not available in 6.2 so we need to be defensive when reading this part of the response. - var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? Array.Empty(); + var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? []; SetResult(message, new StreamAutoClaimIdsOnlyResult(nextStartId, claimedIds, deletedIds)); return true; @@ -2865,7 +2880,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes pendingMessageCount: (int)arr[0].AsRedisValue(), lowestId: arr[1].AsRedisValue(), highestId: arr[2].AsRedisValue(), - consumers: consumers ?? Array.Empty()); + consumers: consumers ?? []); SetResult(message, pendingInfo); return true; @@ -2950,7 +2965,7 @@ protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) // 4) "18.2" if (result.Resp2TypeArray != ResultType.Array || result.IsNull) { - return Array.Empty(); + return []; } return StreamNameValueEntryProcessor.Instance.ParseArray(result, false, out _, null)!; // ! because we checked null above } @@ -3138,7 +3153,7 @@ private sealed class SentinelGetSentinelAddressesProcessor : ResultProcessor endPoints = new List(); + List endPoints = []; switch (result.Resp2TypeArray) { @@ -3172,7 +3187,7 @@ private sealed class SentinelGetReplicaAddressesProcessor : ResultProcessor endPoints = new List(); + List endPoints = []; switch (result.Resp2TypeArray) { @@ -3266,7 +3281,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes T[] arr; if (items.IsEmpty) { - arr = Array.Empty(); + arr = []; } else { diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs new file mode 100644 index 000000000..3d6851a6e --- /dev/null +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs @@ -0,0 +1,245 @@ +using System; + +namespace StackExchange.Redis; + +internal sealed class VectorSetSimilaritySearchMessage( + int db, + CommandFlags flags, + RedisKey key, + RedisValue member, + ReadOnlyMemory vector, + int? count, + bool withScores, + bool withAttributes, + double? epsilon, + int? searchExplorationFactor, + string? filterExpression, + int? maxFilteringEffort, + bool useExactSearch, + bool disableThreading) : Message(db, flags, RedisCommand.VSIM) +{ + private readonly VsimFlags _flags = + (count.HasValue ? VsimFlags.Count : 0) | + (withScores ? VsimFlags.WithScores : 0) | + (withAttributes ? VsimFlags.WithAttributes : 0) | + (useExactSearch ? VsimFlags.UseExactSearch : 0) | + (disableThreading ? VsimFlags.DisableThreading : 0) | + (epsilon.HasValue ? VsimFlags.Epsilon : 0) | + (searchExplorationFactor.HasValue ? VsimFlags.SearchExplorationFactor : 0) | + (maxFilteringEffort.HasValue ? VsimFlags.MaxFilteringEffort : 0); + + private readonly double _epsilon = epsilon.GetValueOrDefault(); + private readonly int _count = count.GetValueOrDefault(); + private readonly int _searchExplorationFactor = searchExplorationFactor.GetValueOrDefault(); + private readonly int _maxFilteringEffort = maxFilteringEffort.GetValueOrDefault(); + + [Flags] + private enum VsimFlags + { + None = 0, + Count = 1 << 0, + WithScores = 1 << 1, + WithAttributes = 1 << 2, + UseExactSearch = 1 << 3, + DisableThreading = 1 << 4, + Epsilon = 1 << 5, + SearchExplorationFactor = 1 << 6, + MaxFilteringEffort = 1 << 7, + } + + private bool HasFlag(VsimFlags flag) => (_flags & flag) != 0; + + public override int ArgCount => GetArgCount(VectorSetAddMessage.UseFp32); + + private int GetArgCount(bool useFp32) + { + int argCount = 2; // {key} and "ELE {member}", "FP32 {vector}" or "VALUES {num}" + if (member.IsNull && !useFp32) + { + argCount += vector.Length; // {vector} in the VALUES case + } + + if (HasFlag(VsimFlags.WithScores)) argCount++; // [WITHSCORES] + if (HasFlag(VsimFlags.WithAttributes)) argCount++; // [WITHATTRIBS] + if (HasFlag(VsimFlags.Count)) argCount += 2; // [COUNT {count}] + if (HasFlag(VsimFlags.Epsilon)) argCount += 2; // [EPSILON {epsilon}] + if (HasFlag(VsimFlags.SearchExplorationFactor)) argCount += 2; // [EF {search-exploration-factor}] + if (filterExpression is not null) argCount += 2; // [FILTER {filterExpression}] + if (HasFlag(VsimFlags.MaxFilteringEffort)) argCount += 2; // [FILTER-EF {max-filtering-effort}] + if (HasFlag(VsimFlags.UseExactSearch)) argCount++; // [TRUTH] + if (HasFlag(VsimFlags.DisableThreading)) argCount++; // [NOTHREAD] + return argCount; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + var useFp32 = VectorSetAddMessage.UseFp32; // avoid race in debug mode + physical.WriteHeader(Command, GetArgCount(useFp32)); + + // Write key + physical.Write(key); + + // Write search target: either "ELE {member}" or vector data + if (!member.IsNull) + { + // Member-based search: "ELE {member}" + physical.WriteBulkString("ELE"u8); + physical.WriteBulkString(member); + } + else + { + // Vector-based search: either "FP32 {vector}" or "VALUES {num} {vector}" + if (useFp32) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(System.Runtime.InteropServices.MemoryMarshal.AsBytes(vector.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(vector.Length); + foreach (var val in vector.Span) + { + physical.WriteBulkString(val); + } + } + } + + if (HasFlag(VsimFlags.WithScores)) + { + physical.WriteBulkString("WITHSCORES"u8); + } + + if (HasFlag(VsimFlags.WithAttributes)) + { + physical.WriteBulkString("WITHATTRIBS"u8); + } + + // Write optional parameters + if (HasFlag(VsimFlags.Count)) + { + physical.WriteBulkString("COUNT"u8); + physical.WriteBulkString(_count); + } + + if (HasFlag(VsimFlags.Epsilon)) + { + physical.WriteBulkString("EPSILON"u8); + physical.WriteBulkString(_epsilon); + } + + if (HasFlag(VsimFlags.SearchExplorationFactor)) + { + physical.WriteBulkString("EF"u8); + physical.WriteBulkString(_searchExplorationFactor); + } + + if (filterExpression is not null) + { + physical.WriteBulkString("FILTER"u8); + physical.WriteBulkString(filterExpression); + } + + if (HasFlag(VsimFlags.MaxFilteringEffort)) + { + physical.WriteBulkString("FILTER-EF"u8); + physical.WriteBulkString(_maxFilteringEffort); + } + + if (HasFlag(VsimFlags.UseExactSearch)) + { + physical.WriteBulkString("TRUTH"u8); + } + + if (HasFlag(VsimFlags.DisableThreading)) + { + physical.WriteBulkString("NOTHREAD"u8); + } + } + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); + + /* + private int GetArgCount(bool useFp32) + { + var count = 4; // key, element and either "FP32 {vector}" or VALUES {num}" + if (reducedDimensions.HasValue) count += 2; // [REDUCE {dim}] + + if (!useFp32) count += values.Length; // {vector} in the VALUES case + + if (useCheckAndSet) count++; // [CAS] + count += quantizationType switch + { + VectorQuantizationType.None or VectorQuantizationType.Binary => 1, // [NOQUANT] or [BIN] + VectorQuantizationType.Int8 => 0, // implicit + _ => throw new ArgumentOutOfRangeException(nameof(quantizationType)), + }; + + if (buildExplorationFactor.HasValue) count += 2; // [EF {build-exploration-factor}] + if (attributesJson is not null) count += 2; // [SETATTR {attributes}] + if (maxConnections.HasValue) count += 2; // [M {numlinks}] + return count; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + bool useFp32 = UseFp32; // snapshot to avoid race in debug scenarios + physical.WriteHeader(Command, GetArgCount(useFp32)); + physical.Write(key); + if (reducedDimensions.HasValue) + { + physical.WriteBulkString("REDUCE"u8); + physical.WriteBulkString(reducedDimensions.GetValueOrDefault()); + } + if (useFp32) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(values.Length); + foreach (var val in values.Span) + { + physical.WriteBulkString(val); + } + } + physical.WriteBulkString(element); + if (useCheckAndSet) physical.WriteBulkString("CAS"u8); + + switch (quantizationType) + { + case VectorQuantizationType.Int8: + break; + case VectorQuantizationType.None: + physical.WriteBulkString("NOQUANT"u8); + break; + case VectorQuantizationType.Binary: + physical.WriteBulkString("BIN"u8); + break; + default: + throw new ArgumentOutOfRangeException(nameof(quantizationType)); + } + if (buildExplorationFactor.HasValue) + { + physical.WriteBulkString("EF"u8); + physical.WriteBulkString(buildExplorationFactor.GetValueOrDefault()); + } + if (attributesJson is not null) + { + physical.WriteBulkString("SETATTR"u8); + physical.WriteBulkString(attributesJson); + } + if (maxConnections.HasValue) + { + physical.WriteBulkString("M"u8); + physical.WriteBulkString(maxConnections.GetValueOrDefault()); + } + } + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); + */ +} diff --git a/src/StackExchange.Redis/VectorSimilarityResult.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs similarity index 85% rename from src/StackExchange.Redis/VectorSimilarityResult.cs rename to src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs index 55c961d7d..f656da607 100644 --- a/src/StackExchange.Redis/VectorSimilarityResult.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis; /// Represents a result from vector similarity search operations. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] -public readonly struct VectorSimilarityResult(RedisValue member, double score = double.NaN, string? attributesJson = null) +public readonly struct VectorSetSimilaritySearchResult(RedisValue member, double score = double.NaN, string? attributesJson = null) { /// /// The member name/identifier in the vectorset. From f755e7a4c46b8fd7d67cfb21d12551aecad1d1d8 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 12 Aug 2025 15:28:01 +0100 Subject: [PATCH 10/32] fixins --- .../VectorSetSimilaritySearchMessage.cs | 2 +- .../VectorSetIntegrationTests.cs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs index 3d6851a6e..34a009e02 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs @@ -53,7 +53,7 @@ private enum VsimFlags private int GetArgCount(bool useFp32) { - int argCount = 2; // {key} and "ELE {member}", "FP32 {vector}" or "VALUES {num}" + int argCount = 3; // {key} and "ELE {member}", "FP32 {vector}" or "VALUES {num}" if (member.IsNull && !useFp32) { argCount += vector.Length; // {vector} in the VALUES case diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 2df8605d4..32aacfe23 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -424,10 +424,14 @@ public async Task VectorSetGetLinks() using var links = await db.VectorSetGetLinksAsync(key, "element1"); Assert.NotNull(links); + foreach (var link in links.Span) + { + Log(link.ToString()); + } + var linksArray = links.Span.ToArray(); - // Should contain the other elements - Assert.Equal(2, linksArray.Length); + // Should contain the other elements (note there can be transient duplicates, so: contains, not exact) Assert.Contains("element2", linksArray); Assert.Contains("element3", linksArray); } @@ -469,12 +473,11 @@ public async Task VectorSetGetLinksWithScores() Assert.True(link.Score >= 0.0); // Similarity scores should be non-negative }); - // Should contain the other elements - Assert.Equal(2, linksArray.Length); + // Should contain the other elements (note there can be transient duplicates, so: contains, not exact) Assert.Contains(linksArray, l => l.Member == "element2"); Assert.Contains(linksArray, l => l.Member == "element3"); - Assert.True(linksArray.Single(l => l.Member == "element2").Score > 0.9); // similar - Assert.True(linksArray.Single(l => l.Member == "element3").Score < 0.8); // less-so + Assert.True(linksArray.First(l => l.Member == "element2").Score > 0.9); // similar + Assert.True(linksArray.First(l => l.Member == "element3").Score < 0.8); // less-so } } From 0ed581e1e403bd39b2cac180ceda7e0e773b3f54 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 13 Aug 2025 12:45:33 +0100 Subject: [PATCH 11/32] core for fast-hash --- src/StackExchange.Redis/FastHash.Literals.cs | 18 +++ src/StackExchange.Redis/FastHash.cs | 116 +++++++++++++++ src/StackExchange.Redis/RawResult.cs | 8 +- src/StackExchange.Redis/ResultProcessor.cs | 47 +++++- .../VectorSetAddMessage.cs | 2 +- .../FastHashBenchmarks.cs | 139 ++++++++++++++++++ .../StackExchange.Redis.Benchmarks/Program.cs | 19 ++- .../StackExchange.Redis.Benchmarks.csproj | 1 + .../FastHashTests.cs | 78 ++++++++++ 9 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 src/StackExchange.Redis/FastHash.Literals.cs create mode 100644 src/StackExchange.Redis/FastHash.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs create mode 100644 tests/StackExchange.Redis.Tests/FastHashTests.cs diff --git a/src/StackExchange.Redis/FastHash.Literals.cs b/src/StackExchange.Redis/FastHash.Literals.cs new file mode 100644 index 000000000..d802b939e --- /dev/null +++ b/src/StackExchange.Redis/FastHash.Literals.cs @@ -0,0 +1,18 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +// See FastHashTests for how these are validated and enforced. When adding new values, use any +// value and run the tests - this will tell you the correct value. +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "To better represent the expected literals")] +internal static partial class FastHash +{ +#pragma warning disable SA1300, SA1303 + public static class Length4 + { + public const long size = 1702521203; + public static ReadOnlySpan size_u8 => "size"u8; + } +#pragma warning restore SA1300, SA1303 +} diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs new file mode 100644 index 000000000..71a616ed1 --- /dev/null +++ b/src/StackExchange.Redis/FastHash.cs @@ -0,0 +1,116 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace StackExchange.Redis; + +/// +/// This type is intended to provide fast hashing functions for small strings, for example well-known +/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended +/// for general purpose hashing. All matches must also perform a sequence equality check. +/// +internal static partial class FastHash +{ + public static long Hash64(this ReadOnlySequence value) + { +#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + var first = value.FirstSpan; +#else + var first = value.First.Span; +#endif + return first.Length >= sizeof(long) || value.IsSingleSegment + ? first.Hash64() : SlowHash64(value); + + static long SlowHash64(ReadOnlySequence value) + { + Span buffer = stackalloc byte[sizeof(long)]; + if (value.Length < sizeof(long)) + { + value.CopyTo(buffer); + buffer.Slice((int)value.Length).Clear(); + } + else + { + value.Slice(0, sizeof(long)).CopyTo(buffer); + } + return BitConverter.IsLittleEndian + ? Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer)) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + } + + public static long Hash64(this scoped ReadOnlySpan value) + { + if (BitConverter.IsLittleEndian) + { + ref byte data = ref MemoryMarshal.GetReference(value); + return value.Length switch + { + 0 => 0, + 1 => data, // 0000000A + 2 => Unsafe.ReadUnaligned(ref data), // 000000BA + 3 => Unsafe.ReadUnaligned(ref data) | // 000000BA + (Unsafe.Add(ref data, 2) << 16), // 00000C00 + 4 => Unsafe.ReadUnaligned(ref data), // 0000DCBA + 5 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA + ((long)Unsafe.Add(ref data, 4) << 32), // 000E0000 + 6 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA + ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32), // 00FE0000 + 7 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA + ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32) | // 00FE0000 + ((long)Unsafe.Add(ref data, 6) << 48), // 0G000000 + _ => Unsafe.ReadUnaligned(ref data), // HGFEDCBA + }; + } + +#pragma warning disable CS0618 // Type or member is obsolete + return Hash64Fallback(value); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Obsolete("Only exists for benchmarks (to show that we don't need to use it) and unit tests (for correctness)")] + internal static unsafe long Hash64Unsafe(scoped ReadOnlySpan value) + { + if (BitConverter.IsLittleEndian) + { + fixed (byte* ptr = &MemoryMarshal.GetReference(value)) + { + return value.Length switch + { + 0 => 0, + 1 => *ptr, // 0000000A + 2 => *(ushort*)ptr, // 000000BA + 3 => *(ushort*)ptr | // 000000BA + (ptr[2] << 16), // 00000C00 + 4 => *(int*)ptr, // 0000DCBA + 5 => (long)*(int*)ptr | // 0000DCBA + ((long)ptr[4] << 32), // 000E0000 + 6 => (long)*(int*)ptr | // 0000DCBA + ((long)*(ushort*)(ptr + 4) << 32), // 00FE0000 + 7 => (long)*(int*)ptr | // 0000DCBA + ((long)*(ushort*)(ptr + 4) << 32) | // 00FE0000 + ((long)ptr[6] << 48), // 0G000000 + _ => *(long*)ptr, // HGFEDCBA + }; + } + } + + return Hash64Fallback(value); + } + + [Obsolete("Only exists for unit tests and fallback")] + internal static long Hash64Fallback(scoped ReadOnlySpan value) + { + if (value.Length < sizeof(long)) + { + Span tmp = stackalloc byte[sizeof(long)]; + value.CopyTo(tmp); // ABC***** + tmp.Slice(value.Length).Clear(); // ABC00000 + return BinaryPrimitives.ReadInt64LittleEndian(tmp); // 00000CBA + } + + return BinaryPrimitives.ReadInt64LittleEndian(value); // HGFEDCBA + } +} diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 55c44652b..1ac9f081a 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -237,10 +237,14 @@ internal bool IsEqual(in CommandBytes expected) return new CommandBytes(Payload).Equals(expected); } - internal unsafe bool IsEqual(byte[]? expected) + internal bool IsEqual(byte[]? expected) { if (expected == null) throw new ArgumentNullException(nameof(expected)); + return IsEqual(new ReadOnlySpan(expected)); + } + internal bool IsEqual(ReadOnlySpan expected) + { var rangeToCheck = Payload; if (expected.Length != rangeToCheck.Length) return false; @@ -250,7 +254,7 @@ internal unsafe bool IsEqual(byte[]? expected) foreach (var segment in rangeToCheck) { var from = segment.Span; - var to = new Span(expected, offset, from.Length); + var to = expected.Slice(offset, from.Length); if (!from.SequenceEqual(to)) return false; offset += from.Length; diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 238041492..0457d33c8 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1900,8 +1900,51 @@ protected override bool TryReadOne(in RawResult result, out RedisValue value) private sealed class VectorSetInfoProcessor : ResultProcessor { - protected override bool - SetResultCore(PhysicalConnection connection, Message message, in RawResult result) => false; + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array) + { + if (result.IsNull) + { + SetResult(message, null); + return true; + } + var quantType = VectorQuantizationType.Unknown; + int vectorDim = 0, maxLevel = 0; + long size = 0, vectorSetUid = 0, hnswMaxNodeUid = 0; + var iter = result.GetItems().GetEnumerator(); + while (iter.MoveNext()) + { + var key = iter.Current; + if (!iter.MoveNext()) break; + var value = iter.Current; + + var len = key.Payload.Length; + switch (len) + { + // case 10 when key.IsEqual("quant-type"u8): + // quantType = value.AsRedisValue() switch + // { + // "NOQUANT" => VectorQuantizationType.None, + // "BIN" => VectorQuantizationType.Binary, + // "INT8" => VectorQuantizationType.Int8, + // _ => VectorQuantizationType.Unknown, + // }; + // break; + case 10 when key.IsEqual("vector-dim"u8) && value.TryGetInt64(out var i64): + vectorDim = checked((int)i64); + break; + case 4 when key.IsEqual("size"u8) && value.TryGetInt64(out var i64): + size = i64; + break; + } + } + + SetResult(message, new VectorSetInfo(quantType, vectorDim, size, maxLevel, vectorSetUid, hnswMaxNodeUid)); + return true; + } + return false; + } } private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs index d41f3df53..b570a67d9 100644 --- a/src/StackExchange.Redis/VectorSetAddMessage.cs +++ b/src/StackExchange.Redis/VectorSetAddMessage.cs @@ -29,7 +29,7 @@ private static bool CheckFp32() // check endianness with a known value internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled); internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled); #else - internal static bool UseFP32 => CanUseFP32; + internal static bool UseFp32 => CanUseFp32; internal static void SuppressFp32() { } internal static void RestoreFp32() { } #endif diff --git a/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs new file mode 100644 index 000000000..78877f163 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs @@ -0,0 +1,139 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace StackExchange.Redis.Benchmarks; + +[Config(typeof(CustomConfig))] +public class FastHashBenchmarks +{ + private const string SharedString = "some-typical-data-for-comparisons"; + private static readonly byte[] SharedUtf8; + private static readonly ReadOnlySequence SharedMultiSegment; + + static FastHashBenchmarks() + { + SharedUtf8 = Encoding.UTF8.GetBytes(SharedString); + + var first = new Segment(SharedUtf8.AsMemory(0, 1), null); + var second = new Segment(SharedUtf8.AsMemory(1), first); + SharedMultiSegment = new ReadOnlySequence(first, 0, second, second.Memory.Length); + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory memory, Segment? previous) + { + Memory = memory; + if (previous is { }) + { + RunningIndex = previous.RunningIndex + previous.Memory.Length; + previous.Next = this; + } + } + } + + private string _sourceString = SharedString; + private ReadOnlyMemory _sourceBytes = SharedUtf8; + private ReadOnlySequence _sourceMultiSegmentBytes = SharedMultiSegment; + private ReadOnlySequence SingleSegmentBytes => new(_sourceBytes); + + [GlobalSetup] + public void Setup() + { + _sourceString = SharedString.Substring(0, Size); + _sourceBytes = SharedUtf8.AsMemory(0, Size); + _sourceMultiSegmentBytes = SharedMultiSegment.Slice(0, Size); + +#pragma warning disable CS0618 // Type or member is obsolete + var bytes = _sourceBytes.Span; + var expected = FastHash.Hash64Fallback(bytes); + + Assert(bytes.Hash64(), nameof(FastHash.Hash64)); + Assert(FastHash.Hash64Unsafe(bytes), nameof(FastHash.Hash64Unsafe)); +#pragma warning restore CS0618 // Type or member is obsolete + Assert(SingleSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (single segment)"); + Assert(_sourceMultiSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (multi segment)"); + + void Assert(long actual, string name) + { + if (actual != expected) + { + throw new InvalidOperationException($"Hash mismatch for {name}, {expected} != {actual}"); + } + } + } + + [ParamsSource(nameof(Sizes))] + public int Size { get; set; } = 7; + + public IEnumerable Sizes => [0, 1, 2, 3, 4, 5, 6, 7, 8, 16]; + + private const int OperationsPerInvoke = 1024; + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)] + public void String() + { + var val = _sourceString; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.GetHashCode(); + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64() + { + var val = _sourceBytes.Span; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.Hash64(); + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64Unsafe() + { + var val = _sourceBytes.Span; + for (int i = 0; i < OperationsPerInvoke; i++) + { +#pragma warning disable CS0618 // Type or member is obsolete + _ = FastHash.Hash64Unsafe(val); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64Fallback() + { + var val = _sourceBytes.Span; + for (int i = 0; i < OperationsPerInvoke; i++) + { +#pragma warning disable CS0618 // Type or member is obsolete + _ = FastHash.Hash64Fallback(val); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64_SingleSegment() + { + var val = SingleSegmentBytes; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.Hash64(); + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64_MultiSegment() + { + var val = _sourceMultiSegmentBytes; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.Hash64(); + } + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/Program.cs b/tests/StackExchange.Redis.Benchmarks/Program.cs index 622d7d593..311202877 100644 --- a/tests/StackExchange.Redis.Benchmarks/Program.cs +++ b/tests/StackExchange.Redis.Benchmarks/Program.cs @@ -1,10 +1,25 @@ -using System.Reflection; +using System; +using System.Reflection; using BenchmarkDotNet.Running; namespace StackExchange.Redis.Benchmarks { internal static class Program { - private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); + private static void Main(string[] args) + { +#if DEBUG + var obj = new FastHashBenchmarks(); + foreach (var size in obj.Sizes) + { + Console.WriteLine($"Size: {size}"); + obj.Size = size; + obj.Setup(); + obj.Hash64(); + } +#else + BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); +#endif + } } } diff --git a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj index be9a3081b..8b335ab02 100644 --- a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj +++ b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj @@ -5,6 +5,7 @@ Release Exe true + enable diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs new file mode 100644 index 000000000..001fe9ea5 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/FastHashTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using Xunit; + +namespace StackExchange.Redis.Tests; + +public class FastHashTests(ITestOutputHelper log) +{ + [Theory] + [MemberData(nameof(HashLiterals))] + public void Hash64(string type, string fieldName, long declaredHash, byte[] expectedBytes, byte[]? actualBytes) + { + Assert.NotNull(actualBytes); // missing the _u8 field, which must exist for corresponding equality test + Assert.Equal(expectedBytes, actualBytes); + var actualHash = FastHash.Hash64(actualBytes); + log.WriteLine($"Hash64: {type} {fieldName}: {actualHash}"); + Assert.Equal(declaredHash, actualHash); + + // check equality between hash implementations +#pragma warning disable CS0618 // Type or member is obsolete + var tmp = FastHash.Hash64Unsafe(actualBytes); + log.WriteLine($"Hash64Unsafe: {tmp}"); + Assert.Equal(actualHash, tmp); + + tmp = FastHash.Hash64Fallback(actualBytes); + log.WriteLine($"Hash64Fallback: {tmp}"); + Assert.Equal(actualHash, tmp); +#pragma warning restore CS0618 // Type or member is obsolete + } + + public static IEnumerable HashLiterals() + { + foreach (var type in typeof(FastHash).GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) + { + if (field.IsLiteral && field.FieldType == typeof(long)) + { + // the expected bytes match fieldName unless there's a [DisplayName] on that field + var expectedBytes = Encoding.UTF8.GetBytes( + field.GetCustomAttribute()?.DisplayName + ?? field.Name); + var declaredHash = (long)field.GetRawConstantValue()!; + + byte[]? actualBytes = null; + var u8Prop = type.GetProperty(field.Name + "_u8"); + if (u8Prop != null && u8Prop.PropertyType == typeof(ReadOnlySpan)) + { + actualBytes = ReadBytes(u8Prop); + } + yield return [type.Name, field.Name, declaredHash, expectedBytes, actualBytes]; + } + } + } + } + + private static readonly MethodInfo ReadOnlySpanToArray = typeof(ReadOnlySpan).GetMethod(nameof(ReadOnlySpan.ToArray))!; + private static byte[]? ReadBytes(PropertyInfo prop) + { + var getter = prop.GetMethod; + if (getter is not { IsStatic: true } || getter.ReturnType != typeof(ReadOnlySpan)) return null; + + // we can't use prop.GetValue() because it's a ref struct (cannot be boxed); instead, we need to use ref-emit + DynamicMethod dm = new DynamicMethod(prop.Name, typeof(byte[]), null, typeof(FastHashTests).Module); + ILGenerator il = dm.GetILGenerator(); + var loc = il.DeclareLocal(typeof(ReadOnlySpan)); + il.EmitCall(OpCodes.Call, getter, null); + il.Emit(OpCodes.Stloc, loc); + il.Emit(OpCodes.Ldloca, loc); + il.EmitCall(OpCodes.Call, ReadOnlySpanToArray, null); + il.Emit(OpCodes.Ret); + return (byte[]?)dm.Invoke(null, null); + } +} From d69145dd4a6c5dcc210b146853f400352b16b863 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 13 Aug 2025 14:51:49 +0100 Subject: [PATCH 12/32] VINFO complete --- src/StackExchange.Redis/FastHash.Literals.cs | 41 ++++++++++++- src/StackExchange.Redis/FastHash.cs | 31 ++++++++++ .../Interfaces/IDatabase.cs | 4 +- .../Interfaces/IDatabaseAsync.cs | 4 +- .../KeyspaceIsolation/KeyPrefixed.cs | 4 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 4 +- .../PublicAPI/PublicAPI.Unshipped.txt | 19 ++++--- src/StackExchange.Redis/RedisDatabase.cs | 8 +-- src/StackExchange.Redis/ResultProcessor.cs | 53 +++++++++++------ .../VectorSetAddMessage.cs | 20 +++---- src/StackExchange.Redis/VectorSetInfo.cs | 11 +++- ...zationType.cs => VectorSetQuantization.cs} | 8 +-- .../FastHashTests.cs | 57 +++++++++++++++---- .../KeyPrefixedVectorSetTests.cs | 4 +- .../VectorSetIntegrationTests.cs | 30 ++++++---- 15 files changed, 221 insertions(+), 77 deletions(-) rename src/StackExchange.Redis/{VectorQuantizationType.cs => VectorSetQuantization.cs} (66%) diff --git a/src/StackExchange.Redis/FastHash.Literals.cs b/src/StackExchange.Redis/FastHash.Literals.cs index d802b939e..b5e18bfd0 100644 --- a/src/StackExchange.Redis/FastHash.Literals.cs +++ b/src/StackExchange.Redis/FastHash.Literals.cs @@ -9,10 +9,49 @@ namespace StackExchange.Redis; internal static partial class FastHash { #pragma warning disable SA1300, SA1303 - public static class Length4 + public static class _3 + { + public const long bin = 7235938; + public static ReadOnlySpan bin_u8 => "bin"u8; + + public const long f32 = 3289958; + public static ReadOnlySpan f32_u8 => "f32"u8; + } + + public static class _4 { public const long size = 1702521203; public static ReadOnlySpan size_u8 => "size"u8; + + public const long int8 = 947154537; + public static ReadOnlySpan int8_u8 => "int8"u8; + } + + public static class _8 + { + public const long vset_uid = 7235443114434196342; + public static ReadOnlySpan vset_uid_u8 => "vset-uid"u8; + } + + public static class _9 + { + public const long max_level = 7311142560376316269; + public static ReadOnlySpan max_level_u8 => "max-level"u8; + } + + public static class _10 + { + public const long quant_type = 8751669953979053425; + public static ReadOnlySpan quant_type_u8 => "quant-type"u8; + + public const long vector_dim = 7218551600764380534; + public static ReadOnlySpan vector_dim_u8 => "vector-dim"u8; + } + + public static class _17 + { + public const long hnsw_max_node_uid = 8674334399337295464; + public static ReadOnlySpan hnsw_max_node_uid_u8 => "hnsw-max-node-uid"u8; } #pragma warning restore SA1300, SA1303 } diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs index 71a616ed1..5b26b521c 100644 --- a/src/StackExchange.Redis/FastHash.cs +++ b/src/StackExchange.Redis/FastHash.cs @@ -11,8 +11,36 @@ namespace StackExchange.Redis; /// RESP literals that are usually identifiable by their length and initial bytes; it is not intended /// for general purpose hashing. All matches must also perform a sequence equality check. /// +/// While introduced alongside the VSET work, this is not specific to VSET - and indeed VSET +/// is not a good example of the feature; rather, this is intended for more widespread use. +/* +Example data from the benchmarks; note that string is included only for baseline purposes - we don't actually want +to construct strings when parsing tokens. + +| Method | Size | Mean | Error | StdDev | Median | Op/s | Ratio | RatioSD | Allocated | Alloc Ratio | +|--------------------- |----- |----------:|----------:|----------:|----------:|--------------:|------:|--------:|----------:|------------:| +| String | 16 | 21.376 ns | 0.4164 ns | 0.6483 ns | 21.268 ns | 46,781,518.5 | 1.00 | 0.04 | - | NA | +| Hash64 | 16 | 3.161 ns | 0.0605 ns | 0.0647 ns | 3.148 ns | 316,400,326.5 | 0.15 | 0.01 | - | NA | +| Hash64Unsafe | 16 | 3.820 ns | 0.0747 ns | 0.1072 ns | 3.811 ns | 261,789,013.8 | 0.18 | 0.01 | - | NA | +| Hash64Fallback | 16 | 19.461 ns | 0.2954 ns | 0.2763 ns | 19.496 ns | 51,383,837.0 | 0.91 | 0.03 | - | NA | +| Hash64_SingleSegment | 16 | 9.477 ns | 0.1877 ns | 0.3705 ns | 9.464 ns | 105,519,833.1 | 0.44 | 0.02 | - | NA | +| Hash64_MultiSegment | 16 | 82.778 ns | 1.6255 ns | 2.3313 ns | 82.475 ns | 12,080,568.6 | 3.88 | 0.16 | - | NA | +*/ internal static partial class FastHash { + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + public sealed class LiteralAttribute(string token) : Attribute + { + public string Token => token; + } + + // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves + // our entropy, but is still useful when case doesn't matter. + private const long CaseMask = ~0x2020202020202020; + + public static long Hash64CI(this ReadOnlySequence value) + => value.Hash64() & CaseMask; + public static long Hash64(this ReadOnlySequence value) { #if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER @@ -41,6 +69,9 @@ static long SlowHash64(ReadOnlySequence value) } } + public static long Hash64CI(this scoped ReadOnlySpan value) + => value.Hash64() & CaseMask; + public static long Hash64(this scoped ReadOnlySpan value) { if (BitConverter.IsLittleEndian) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 209bc7960..8afb56d3c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3440,7 +3440,7 @@ IEnumerable SortedSetScan( /// The element name. /// The vector data. /// Optional dimension reduction using random projection (REDUCE parameter). - /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. + /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. /// Optional HNSW build exploration factor (EF parameter, default: 200). /// Optional maximum connections per HNSW node (M parameter, default: 16). /// Optional check-and-set mode for partial threading (CAS parameter). @@ -3454,7 +3454,7 @@ bool VectorSetAdd( RedisValue element, ReadOnlyMemory values, int? reducedDimensions = null, - VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + VectorSetQuantization quantization = VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 3436c73a7..fd6cb30d1 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -848,14 +848,14 @@ IAsyncEnumerable SortedSetScanAsync( // Vector Set operations - /// + /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetAddAsync( RedisKey key, RedisValue element, ReadOnlyMemory values, int? reducedDimensions = null, - VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + VectorSetQuantization quantization = VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index cd060cbd5..5ffb786c0 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -821,13 +821,13 @@ public Task VectorSetAddAsync( RedisValue element, ReadOnlyMemory values, int? reducedDimensions = null, - VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + VectorSetQuantization quantization = VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetAddAsync(ToInner(key), element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + Inner.VectorSetAddAsync(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.VectorSetLengthAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index a8c42d20f..f01ab13fb 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -811,13 +811,13 @@ public bool VectorSetAdd( RedisValue element, ReadOnlyMemory values, int? reducedDimensions = null, - VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + VectorSetQuantization quantization = VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetAdd(ToInner(key), element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + Inner.VectorSetAdd(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.VectorSetLength(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ec9fd22d3..606e17fb5 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable [SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! -[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int [SER001]StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? @@ -15,7 +15,7 @@ [SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? [SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorQuantizationType quantizationType = StackExchange.Redis.VectorQuantizationType.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! @@ -30,25 +30,26 @@ [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.VectorQuantizationType -[SER001]StackExchange.Redis.VectorQuantizationType.Binary = 3 -> StackExchange.Redis.VectorQuantizationType -[SER001]StackExchange.Redis.VectorQuantizationType.Int8 = 2 -> StackExchange.Redis.VectorQuantizationType -[SER001]StackExchange.Redis.VectorQuantizationType.None = 1 -> StackExchange.Redis.VectorQuantizationType -[SER001]StackExchange.Redis.VectorQuantizationType.Unknown = 0 -> StackExchange.Redis.VectorQuantizationType [SER001]StackExchange.Redis.VectorSetInfo [SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int [SER001]StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long [SER001]StackExchange.Redis.VectorSetInfo.Length.get -> long [SER001]StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int -[SER001]StackExchange.Redis.VectorSetInfo.QuantizationType.get -> StackExchange.Redis.VectorQuantizationType +[SER001]StackExchange.Redis.VectorSetInfo.Quantization.get -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetInfo.QuantizationRaw.get -> string? [SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void -[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorQuantizationType quantizationType, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorSetQuantization quantization, string? quantizationRaw, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void [SER001]StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long [SER001]StackExchange.Redis.VectorSetLink [SER001]StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue [SER001]StackExchange.Redis.VectorSetLink.Score.get -> double [SER001]StackExchange.Redis.VectorSetLink.VectorSetLink() -> void [SER001]StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void +[SER001]StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Binary = 3 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Int8 = 2 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.None = 1 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Unknown = 0 -> StackExchange.Redis.VectorSetQuantization [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.AttributesJson.get -> string? [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Member.get -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index c44e5b71a..5426a9fa5 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5727,14 +5727,14 @@ public bool VectorSetAdd( RedisValue element, ReadOnlyMemory values, int? reducedDimensions = null, - VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + VectorSetQuantization quantization = VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, CommandFlags flags = CommandFlags.None) { - var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); return ExecuteSync(msg, ResultProcessor.Boolean); } @@ -5880,14 +5880,14 @@ public Task VectorSetAddAsync( RedisValue element, ReadOnlyMemory values, int? reducedDimensions = null, - VectorQuantizationType quantizationType = VectorQuantizationType.Int8, + VectorSetQuantization quantization = VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, CommandFlags flags = CommandFlags.None) { - var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantizationType, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); return ExecuteAsync(msg, ResultProcessor.Boolean); } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 0457d33c8..65b0408eb 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1909,9 +1909,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, null); return true; } - var quantType = VectorQuantizationType.Unknown; + var quantType = VectorSetQuantization.Unknown; + string? quantTypeRaw = null; int vectorDim = 0, maxLevel = 0; - long size = 0, vectorSetUid = 0, hnswMaxNodeUid = 0; + long size = 0, vsetUid = 0, hnswMaxNodeUid = 0; var iter = result.GetItems().GetEnumerator(); while (iter.MoveNext()) { @@ -1920,27 +1921,47 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var value = iter.Current; var len = key.Payload.Length; - switch (len) + var keyHash = key.Payload.Hash64(); + switch (key.Payload.Length) { - // case 10 when key.IsEqual("quant-type"u8): - // quantType = value.AsRedisValue() switch - // { - // "NOQUANT" => VectorQuantizationType.None, - // "BIN" => VectorQuantizationType.Binary, - // "INT8" => VectorQuantizationType.Int8, - // _ => VectorQuantizationType.Unknown, - // }; - // break; - case 10 when key.IsEqual("vector-dim"u8) && value.TryGetInt64(out var i64): + case 4 when keyHash == FastHash._4.size && key.IsEqual(FastHash._4.size_u8) && value.TryGetInt64(out var i64): + size = i64; + break; + case 8 when keyHash == FastHash._8.vset_uid && key.IsEqual(FastHash._8.vset_uid_u8) && value.TryGetInt64(out var i64): + vsetUid = i64; + break; + case 9 when keyHash == FastHash._9.max_level && key.IsEqual(FastHash._9.max_level_u8) && value.TryGetInt64(out var i64): + maxLevel = checked((int)i64); + break; + case 10 when keyHash == FastHash._10.vector_dim && key.IsEqual(FastHash._10.vector_dim_u8) && value.TryGetInt64(out var i64): vectorDim = checked((int)i64); break; - case 4 when key.IsEqual("size"u8) && value.TryGetInt64(out var i64): - size = i64; + case 10 when keyHash == FastHash._10.quant_type && key.IsEqual(FastHash._10.quant_type_u8): + var qHash = value.Payload.Hash64(); + switch (value.Payload.Length) + { + case 3 when qHash == FastHash._3.bin && value.IsEqual(FastHash._3.bin_u8): + quantType = VectorSetQuantization.Binary; + break; + case 3 when qHash == FastHash._3.f32 && value.IsEqual(FastHash._3.f32_u8): + quantType = VectorSetQuantization.None; + break; + case 4 when qHash == FastHash._4.int8 && value.IsEqual(FastHash._4.int8_u8): + quantType = VectorSetQuantization.Int8; + break; + default: + quantTypeRaw = value.GetString(); + quantType = VectorSetQuantization.Unknown; + break; + } + break; + case 17 when keyHash == FastHash._17.hnsw_max_node_uid && key.IsEqual(FastHash._17.hnsw_max_node_uid_u8) && value.TryGetInt64(out var i64): + hnswMaxNodeUid = i64; break; } } - SetResult(message, new VectorSetInfo(quantType, vectorDim, size, maxLevel, vectorSetUid, hnswMaxNodeUid)); + SetResult(message, new VectorSetInfo(quantType, quantTypeRaw, vectorDim, size, maxLevel, vsetUid, hnswMaxNodeUid)); return true; } return false; diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs index b570a67d9..26bd739be 100644 --- a/src/StackExchange.Redis/VectorSetAddMessage.cs +++ b/src/StackExchange.Redis/VectorSetAddMessage.cs @@ -11,7 +11,7 @@ internal sealed class VectorSetAddMessage( RedisValue element, ReadOnlyMemory values, int? reducedDimensions, - VectorQuantizationType quantizationType, + VectorSetQuantization quantization, int? buildExplorationFactor, int? maxConnections, bool useCheckAndSet, @@ -44,11 +44,11 @@ private int GetArgCount(bool useFp32) if (!useFp32) count += values.Length; // {vector} in the VALUES case if (useCheckAndSet) count++; // [CAS] - count += quantizationType switch + count += quantization switch { - VectorQuantizationType.None or VectorQuantizationType.Binary => 1, // [NOQUANT] or [BIN] - VectorQuantizationType.Int8 => 0, // implicit - _ => throw new ArgumentOutOfRangeException(nameof(quantizationType)), + VectorSetQuantization.None or VectorSetQuantization.Binary => 1, // [NOQUANT] or [BIN] + VectorSetQuantization.Int8 => 0, // implicit + _ => throw new ArgumentOutOfRangeException(nameof(quantization)), }; if (buildExplorationFactor.HasValue) count += 2; // [EF {build-exploration-factor}] @@ -84,18 +84,18 @@ protected override void WriteImpl(PhysicalConnection physical) physical.WriteBulkString(element); if (useCheckAndSet) physical.WriteBulkString("CAS"u8); - switch (quantizationType) + switch (quantization) { - case VectorQuantizationType.Int8: + case VectorSetQuantization.Int8: break; - case VectorQuantizationType.None: + case VectorSetQuantization.None: physical.WriteBulkString("NOQUANT"u8); break; - case VectorQuantizationType.Binary: + case VectorSetQuantization.Binary: physical.WriteBulkString("BIN"u8); break; default: - throw new ArgumentOutOfRangeException(nameof(quantizationType)); + throw new ArgumentOutOfRangeException(nameof(quantization)); } if (buildExplorationFactor.HasValue) { diff --git a/src/StackExchange.Redis/VectorSetInfo.cs b/src/StackExchange.Redis/VectorSetInfo.cs index a755ae576..c9277eae5 100644 --- a/src/StackExchange.Redis/VectorSetInfo.cs +++ b/src/StackExchange.Redis/VectorSetInfo.cs @@ -8,7 +8,8 @@ namespace StackExchange.Redis; /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] public readonly struct VectorSetInfo( - VectorQuantizationType quantizationType, + VectorSetQuantization quantization, + string? quantizationRaw, int dimension, long length, int maxLevel, @@ -18,7 +19,13 @@ public readonly struct VectorSetInfo( /// /// The quantization type used for vectors in this vectorset. /// - public VectorQuantizationType QuantizationType { get; } = quantizationType; + public VectorSetQuantization Quantization { get; } = quantization; + + /// + /// The raw representation of the quantization type used for vectors in this vectorset. This is only + /// populated if the is . + /// + public string? QuantizationRaw { get; } = quantizationRaw; /// /// The number of dimensions in each vector. diff --git a/src/StackExchange.Redis/VectorQuantizationType.cs b/src/StackExchange.Redis/VectorSetQuantization.cs similarity index 66% rename from src/StackExchange.Redis/VectorQuantizationType.cs rename to src/StackExchange.Redis/VectorSetQuantization.cs index 66a210714..0e5a3304f 100644 --- a/src/StackExchange.Redis/VectorQuantizationType.cs +++ b/src/StackExchange.Redis/VectorSetQuantization.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis; /// Specifies the quantization type for vectors in a vectorset. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] -public enum VectorQuantizationType +public enum VectorSetQuantization { /// /// Unknown or unrecognized quantization type. @@ -14,17 +14,17 @@ public enum VectorQuantizationType Unknown, /// - /// No quantization (full precision). + /// No quantization (full precision). This maps to "NOQUANT" or "f32". /// None, /// - /// 8-bit integer quantization (default). + /// 8-bit integer quantization (default). This maps to "Q8" or "int8". /// Int8, /// - /// Binary quantization. + /// Binary quantization. This maps to "BIN" or "bin". /// Binary, } diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs index 001fe9ea5..f08b9d1b3 100644 --- a/tests/StackExchange.Redis.Tests/FastHashTests.cs +++ b/tests/StackExchange.Redis.Tests/FastHashTests.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; +using System.Globalization; using System.Reflection; using System.Reflection.Emit; using System.Text; @@ -10,40 +10,77 @@ namespace StackExchange.Redis.Tests; public class FastHashTests(ITestOutputHelper log) { + [Fact] + public void CaseInsensitiveHash() + { + Assert.NotEqual("abc"u8.Hash64(), "ABC"u8.Hash64()); + Assert.Equal("abc"u8.Hash64CI(), "ABC"u8.Hash64CI()); + } + + [Fact] + public void CaseSensitiveEquals() + { + Assert.True("ABC"u8.SequenceEqual("ABC"u8), "same"); + Assert.False("abc"u8.SequenceEqual("ABC"u8), "off-by-case"); + } + [Theory] [MemberData(nameof(HashLiterals))] - public void Hash64(string type, string fieldName, long declaredHash, byte[] expectedBytes, byte[]? actualBytes) + public void Hash64(string type, string fieldName, long declaredHash, byte[] expectedBytes, byte[]? actualBytes, int? length) { Assert.NotNull(actualBytes); // missing the _u8 field, which must exist for corresponding equality test Assert.Equal(expectedBytes, actualBytes); var actualHash = FastHash.Hash64(actualBytes); - log.WriteLine($"Hash64: {type} {fieldName}: {actualHash}"); + log.WriteLine($"{nameof(FastHash.Hash64)}: {type} {fieldName}: {actualHash}"); Assert.Equal(declaredHash, actualHash); + if (length.HasValue) + { + Assert.Equal(length.Value, actualBytes.Length); + } + // check equality between hash implementations #pragma warning disable CS0618 // Type or member is obsolete var tmp = FastHash.Hash64Unsafe(actualBytes); - log.WriteLine($"Hash64Unsafe: {tmp}"); + log.WriteLine($"{nameof(FastHash.Hash64Unsafe)}: {tmp}"); Assert.Equal(actualHash, tmp); tmp = FastHash.Hash64Fallback(actualBytes); - log.WriteLine($"Hash64Fallback: {tmp}"); + log.WriteLine($"{nameof(FastHash.Hash64Fallback)}: {tmp}"); Assert.Equal(actualHash, tmp); #pragma warning restore CS0618 // Type or member is obsolete } public static IEnumerable HashLiterals() { - foreach (var type in typeof(FastHash).GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + var pending = new Queue(); + pending.Enqueue(typeof(FastHash)); + while (pending.TryDequeue(out var type)) { + // dive into nested types + foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + { + pending.Enqueue(nested); + } + + // enforce the length if the type is named "_{N}" + object? length = null; + if (type.Name.StartsWith("_") && int.TryParse(type.Name.Substring(1), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int i)) + { + length = i; + } + + // check for relevant fields foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) { + if (field.Name == "CaseMask" && type == typeof(FastHash)) continue; // not a hash + if (field.IsLiteral && field.FieldType == typeof(long)) { - // the expected bytes match fieldName unless there's a [DisplayName] on that field + // the expected bytes match fieldName (using - for _) unless there's a [Literal] on that field var expectedBytes = Encoding.UTF8.GetBytes( - field.GetCustomAttribute()?.DisplayName - ?? field.Name); + field.GetCustomAttribute()?.Token + ?? field.Name.Replace("_", "-")); var declaredHash = (long)field.GetRawConstantValue()!; byte[]? actualBytes = null; @@ -52,7 +89,7 @@ public void Hash64(string type, string fieldName, long declaredHash, byte[] expe { actualBytes = ReadBytes(u8Prop); } - yield return [type.Name, field.Name, declaredHash, expectedBytes, actualBytes]; + yield return [type.Name, field.Name, declaredHash, expectedBytes, actualBytes, length]; } } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs index 73ed61c41..b3cff51a5 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -60,7 +60,7 @@ public void VectorSetAdd_WithAllParameters() "element1", vector, reducedDimensions: 64, - quantizationType: VectorQuantizationType.Binary, + quantization: VectorSetQuantization.Binary, buildExplorationFactor: 300, maxConnections: 32, useCheckAndSet: true, @@ -72,7 +72,7 @@ public void VectorSetAdd_WithAllParameters() "element1", vector, 64, - VectorQuantizationType.Binary, + VectorSetQuantization.Binary, 300, 32, true, diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 32aacfe23..d6ea9de06 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -57,10 +57,10 @@ public async Task VectorSetAdd_WithAttributes() } [Theory] - [InlineData(VectorQuantizationType.Int8)] - [InlineData(VectorQuantizationType.None)] - [InlineData(VectorQuantizationType.Binary)] - public async Task VectorSetAdd_WithEverything(VectorQuantizationType quantizationType) + [InlineData(VectorSetQuantization.Int8)] + [InlineData(VectorSetQuantization.None)] + [InlineData(VectorSetQuantization.Binary)] + public async Task VectorSetAdd_WithEverything(VectorSetQuantization quantization) { await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); @@ -77,7 +77,7 @@ public async Task VectorSetAdd_WithEverything(VectorQuantizationType quantizatio vector.AsMemory(), attributesJson: attributes, useCheckAndSet: true, - quantizationType: quantizationType, + quantization: quantization, reducedDimensions: 64, buildExplorationFactor: 300, maxConnections: 32); @@ -225,8 +225,11 @@ public async Task VectorSetRemove() Assert.False(exists); } - [Fact] - public async Task VectorSetInfo() + [Theory] + [InlineData(VectorSetQuantization.Int8)] + [InlineData(VectorSetQuantization.Binary)] + [InlineData(VectorSetQuantization.None)] + public async Task VectorSetInfo(VectorSetQuantization quantization) { await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); @@ -235,14 +238,19 @@ public async Task VectorSetInfo() await db.KeyDeleteAsync(key); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; - await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), quantization: quantization); var info = await db.VectorSetInfoAsync(key); Assert.NotNull(info); - Assert.Equal(5, info.Value.Dimension); - Assert.Equal(1, info.Value.Length); - Assert.Equal(VectorQuantizationType.Int8, info.Value.QuantizationType); + var v = info.GetValueOrDefault(); + Assert.Equal(5, v.Dimension); + Assert.Equal(1, v.Length); + Assert.Equal(quantization, v.Quantization); + Assert.Null(v.QuantizationRaw); // Should be null for known quant types + + Assert.NotEqual(0, v.VectorSetUid); + Assert.NotEqual(0, v.HnswMaxNodeUid); } [Fact] From 8933579c45924860dcdf59df626b210f4a151b2b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 13 Aug 2025 16:08:56 +0100 Subject: [PATCH 13/32] VSIM; watch out for RESP3+WITHSCORES+WITHATTRIBS, that's a doozy! --- .../PublicAPI/PublicAPI.Unshipped.txt | 1 + src/StackExchange.Redis/RedisDatabase.cs | 8 +- src/StackExchange.Redis/ResultProcessor.cs | 7 -- .../VectorSetSimilaritySearchMessage.cs | 77 +++++++++++++ .../VectorSetSimilaritySearchResult.cs | 15 +++ tests/StackExchange.Redis.Tests/TestBase.cs | 14 ++- .../VectorSetIntegrationTests.cs | 105 ++++++++++-------- 7 files changed, 171 insertions(+), 56 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 606e17fb5..4b349c738 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ #nullable enable [SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! +[SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! [SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 5426a9fa5..22fe7711c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5839,7 +5839,7 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j maxFilteringEffort, useExactSearch, disableThreading); - return ExecuteSync(msg, ResultProcessor.LeaseVectorSimilarityResult); + return ExecuteSync(msg, msg.GetResultProcessor()); } public Lease? VectorSetSimilaritySearchByMember( @@ -5871,7 +5871,7 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j maxFilteringEffort, useExactSearch, disableThreading); - return ExecuteSync(msg, ResultProcessor.LeaseVectorSimilarityResult); + return ExecuteSync(msg, msg.GetResultProcessor()); } // Vector Set async operations @@ -5992,7 +5992,7 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe maxFilteringEffort, useExactSearch, disableThreading); - return ExecuteAsync(msg, ResultProcessor.LeaseVectorSimilarityResult); + return ExecuteAsync(msg, msg.GetResultProcessor()); } public Task?> VectorSetSimilaritySearchByMemberAsync( @@ -6024,7 +6024,7 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe maxFilteringEffort, useExactSearch, disableThreading); - return ExecuteAsync(msg, ResultProcessor.LeaseVectorSimilarityResult); + return ExecuteAsync(msg, msg.GetResultProcessor()); } } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 65b0408eb..8bf55b04b 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -65,7 +65,6 @@ public static readonly ResultProcessor public static readonly ResultProcessor?> VectorSetLinksWithScores = new VectorSetLinksWithScoresProcessor(); public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); - public static readonly ResultProcessor?> LeaseVectorSimilarityResult = new VectorSetSimilaritySearchProcessor(); public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); @@ -1968,12 +1967,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> - { - protected override bool - SetResultCore(PhysicalConnection connection, Message message, in RawResult result) => false; - } - private sealed class LeaseFloat32Processor : LeaseProcessor { protected override bool TryParse(in RawResult raw, out float parsed) diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs index 34a009e02..9fd6a5a13 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs @@ -33,6 +33,83 @@ internal sealed class VectorSetSimilaritySearchMessage( private readonly int _searchExplorationFactor = searchExplorationFactor.GetValueOrDefault(); private readonly int _maxFilteringEffort = maxFilteringEffort.GetValueOrDefault(); + public ResultProcessor?> GetResultProcessor() => VectorSetSimilaritySearchProcessor.Instance; + private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> + { + // keep local, since we need to know what flags were being sent + public static readonly VectorSetSimilaritySearchProcessor Instance = new(); + private VectorSetSimilaritySearchProcessor() { } + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array && message is VectorSetSimilaritySearchMessage vssm) + { + if (result.IsNull) + { + SetResult(message, null); + return true; + } + + bool withScores = vssm.HasFlag(VsimFlags.WithScores); + bool withAttribs = vssm.HasFlag(VsimFlags.WithAttributes); + + // in RESP3 mode (only), when both are requested, we get a sub-array per item; weird, but true + bool internalNesting = withScores && withAttribs && connection.Protocol is RedisProtocol.Resp3; + + int rowsPerItem = internalNesting ? 2 + : 1 + ((withScores ? 1 : 0) + (withAttribs ? 1 : 0)); // each value is separate root element + + var items = result.GetItems(); + var length = checked((int)items.Length) / rowsPerItem; + var lease = Lease.Create(length, clear: false); + var target = lease.Span; + int count = 0; + var iter = items.GetEnumerator(); + for (int i = 0; i < target.Length && iter.MoveNext(); i++) + { + var member = iter.Current.AsRedisValue(); + double score = double.NaN; + string? attributesJson = null; + + if (internalNesting) + { + if (!iter.MoveNext() || iter.Current.Resp2TypeArray != ResultType.Array) break; + if (!iter.Current.IsNull) + { + var subArray = iter.Current.GetItems(); + if (subArray.Length >= 1 && !subArray[0].TryGetDouble(out score)) break; + if (subArray.Length >= 2) attributesJson = subArray[1].GetString(); + } + } + else + { + if (withScores) + { + if (!iter.MoveNext() || !iter.Current.TryGetDouble(out score)) break; + } + + if (withAttribs) + { + if (!iter.MoveNext()) break; + attributesJson = iter.Current.GetString(); + } + } + + target[i] = new VectorSetSimilaritySearchResult(member, score, attributesJson); + count++; + } + if (count == target.Length) + { + SetResult(message, lease); + return true; + } + lease.Dispose(); // failed to fill? + } + + return false; + } + } + [Flags] private enum VsimFlags { diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs index f656da607..b86bd478f 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs @@ -23,4 +23,19 @@ public readonly struct VectorSetSimilaritySearchResult(RedisValue member, double /// The JSON attributes associated with the member when WITHATTRIBS is used, null otherwise. /// public string? AttributesJson { get; } = attributesJson; + + /// + public override string ToString() + { + if (double.IsNaN(Score)) + { + return AttributesJson is null + ? Member.ToString() + : $"{Member}: {AttributesJson}"; + } + + return AttributesJson is null + ? $"{Member} ({Score})" + : $"{Member} ({Score}): {AttributesJson}"; + } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 94a14ee32..68dbb6055 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -51,7 +51,19 @@ public static void Log(TextWriter output, string message) output?.WriteLine(Time() + ": " + message); } } - protected void Log(string? message, params object[] args) => Output.WriteLine(Time() + ": " + message, args); + + protected void Log(string? message, params object[] args) + { + if (args is { Length: > 0 }) + { + Output.WriteLine(Time() + ": " + message, args); + } + else + { + // avoid "not intended as a format specifier" scenarios + Output.WriteLine(Time() + ": " + message); + } + } protected ProfiledCommandEnumerable Log(ProfilingSession session) { diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index d6ea9de06..70bbba4fa 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -296,12 +296,17 @@ public async Task VectorSetRandomMembers() Assert.True(member == "element1" || member == "element2" || member == "element3")); } - [Fact] - public async Task VectorSetSimilaritySearch_WithVector() + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task VectorSetSimilaritySearch_ByVector(bool withScores, bool withAttributes) { await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); - var key = Me(); + var disambiguator = (withScores ? 1 : 0) + (withAttributes ? 2 : 0); + var key = Me() + disambiguator; await db.KeyDeleteAsync(key); @@ -310,77 +315,89 @@ public async Task VectorSetSimilaritySearch_WithVector() var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; var vector3 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); - await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); + await db.VectorSetAddAsync(key, "element3", vector3.AsMemory(), attributesJson: """{"category":"z"}"""); // Search for vectors similar to vector1 using var results = - await db.VectorSetSimilaritySearchByVectorAsync(key, vector1.AsMemory(), count: 2, withScores: true); + await db.VectorSetSimilaritySearchByVectorAsync( + key, + vector1.AsMemory(), + count: 2, + withScores: withScores, + withAttributes: withAttributes); Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } var resultsArray = results.Span.ToArray(); Assert.True(resultsArray.Length <= 2); Assert.Contains(resultsArray, r => r.Member == "element1"); + var found = resultsArray.First(r => r.Member == "element1"); - // Verify scores are present when withScores is true - Assert.All(resultsArray, r => Assert.False(double.IsNaN(r.Score))); + if (withAttributes) + { + Assert.Equal("""{"category":"x"}""", found.AttributesJson); + } + else + { + Assert.Null(found.AttributesJson); + } + + Assert.NotEqual(withScores, double.IsNaN(found.Score)); } - [Fact] - public async Task VectorSetSimilaritySearch_WithMember() + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task VectorSetSimilaritySearch_ByMember(bool withScores, bool withAttributes) { await using var conn = Create(require: RedisFeatures.v8_0_0_M04); var db = conn.GetDatabase(); - var key = Me(); + var disambiguator = (withScores ? 1 : 0) + (withAttributes ? 2 : 0); + var key = Me() + disambiguator; await db.KeyDeleteAsync(key); var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, "element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); + await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); using var results = - await db.VectorSetSimilaritySearchByMemberAsync(key, "element1", count: 1, withScores: true); + await db.VectorSetSimilaritySearchByMemberAsync( + key, + "element1", + count: 1, + withScores: withScores, + withAttributes: withAttributes); Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } var resultsArray = results.Span.ToArray(); Assert.Single(resultsArray); Assert.Equal("element1", resultsArray[0].Member); - Assert.False(double.IsNaN(resultsArray[0].Score)); - } - - [Fact] - public async Task VectorSetSimilaritySearch_WithAttributes() - { - await using var conn = Create(require: RedisFeatures.v8_0_0_M04); - var db = conn.GetDatabase(); - var key = Me(); - - await db.KeyDeleteAsync(key); - - var vector = new float[] { 1.0f, 2.0f, 3.0f }; - var attributes = """{"category":"test","priority":"high"}"""; - - await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), attributesJson: attributes); - - using var results = await db.VectorSetSimilaritySearchByVectorAsync( - key, - vector.AsMemory(), - count: 1, - withScores: true, - withAttributes: true); - - Assert.NotNull(results); - var result = results.Span[0]; + if (withAttributes) + { + Assert.Equal("""{"category":"x"}""", resultsArray[0].AttributesJson); + } + else + { + Assert.Null(resultsArray[0].AttributesJson); + } - Assert.Equal("element1", result.Member); - Assert.False(double.IsNaN(result.Score)); - Assert.Equal(attributes, result.AttributesJson); + Assert.NotEqual(withScores, double.IsNaN(resultsArray[0].Score)); } [Fact] From 828b4085d0c0bed9929f031f05058d5efaea1e16 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 13 Aug 2025 16:57:11 +0100 Subject: [PATCH 14/32] VSIM filter integration tests --- .../Interfaces/IDatabase.cs | 2 +- .../VectorSetIntegrationTests.cs | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 8afb56d3c..0d299f1b3 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3600,7 +3600,7 @@ bool VectorSetAdd( /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). /// Optional search exploration factor for better recall (EF parameter). - /// Optional filter expression to restrict results (FILTER parameter). + /// Optional filter expression to restrict results (FILTER parameter); . /// Optional maximum filtering attempts (FILTER-EF parameter). /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). /// Whether to run search in main thread (NOTHREAD parameter). diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 70bbba4fa..330407f70 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -1,6 +1,8 @@ using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json; using Xunit; namespace StackExchange.Redis.Tests; @@ -333,6 +335,7 @@ await db.VectorSetSimilaritySearchByVectorAsync( { Log(result.ToString()); } + var resultsArray = results.Span.ToArray(); Assert.True(resultsArray.Length <= 2); @@ -384,6 +387,7 @@ await db.VectorSetSimilaritySearchByMemberAsync( { Log(result.ToString()); } + var resultsArray = results.Span.ToArray(); Assert.Single(resultsArray); @@ -400,6 +404,86 @@ await db.VectorSetSimilaritySearchByMemberAsync( Assert.NotEqual(withScores, double.IsNaN(resultsArray[0].Score)); } + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task VectorSetSimilaritySearch_WithFilter(bool corruptPrefix, bool corruptSuffix) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + Random rand = new Random(); + + float[] vector = new float[50]; + + void ScrambleVector() + { + var arr = vector; + for (int i = 0; i < arr.Length; i++) + { + arr[i] = (float)rand.NextDouble(); + } + } + + string[] regions = new[] { "us-west", "us-east", "eu-west", "eu-east", "ap-south", "ap-north" }; + for (int i = 0; i < 100; i++) + { + var region = regions[rand.Next(regions.Length)]; + var json = (corruptPrefix ? "oops" : "") + + JsonConvert.SerializeObject(new { id = i, region }) + + (corruptSuffix ? "oops" : ""); + ScrambleVector(); + await db.VectorSetAddAsync(key, $"element{i}", vector, attributesJson: json); + } + + ScrambleVector(); + using var results = + await db.VectorSetSimilaritySearchByVectorAsync( + key, + vector, + count: 100, + withScores: true, + withAttributes: true, + filterExpression: ".id >= 30"); + + Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } + + Log($"Total matches: {results.Span.Length}"); + + var resultsArray = results.Span.ToArray(); + if (corruptPrefix) + { + // server short-circuits failure to be no match; we just want to assert + // what the observed behavior *is* + Assert.Empty(resultsArray); + } + else + { + Assert.Equal(70, resultsArray.Length); + Assert.All(resultsArray, r => Assert.True( + r.Score is > 0.0 and < 1.0 && GetId(r.Member!) >= 30)); + } + + static int GetId(string member) + { + if (member.StartsWith("element")) + { + return int.Parse(member.Substring(7), NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + return -1; + } + } + [Fact] public async Task VectorSetSetAttributesJson() { From 697d3125a29ad4da7df14c109eda9ad0a7b632d3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 13 Aug 2025 17:04:27 +0100 Subject: [PATCH 15/32] allow VectorSet as a method prefix (CheckSignatures) --- tests/StackExchange.Redis.Tests/NamingTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/NamingTests.cs b/tests/StackExchange.Redis.Tests/NamingTests.cs index d0474f782..9d9e032ad 100644 --- a/tests/StackExchange.Redis.Tests/NamingTests.cs +++ b/tests/StackExchange.Redis.Tests/NamingTests.cs @@ -193,7 +193,8 @@ private void CheckMethod(MethodInfo method, bool isAsync) || shortName.StartsWith("Script") || shortName.StartsWith("SortedSet") || shortName.StartsWith("String") - || shortName.StartsWith("Stream"); + || shortName.StartsWith("Stream") + || shortName.StartsWith("VectorSet"); Log(fullName + ": " + (isValid ? "valid" : "invalid")); Assert.True(isValid, fullName + ":Prefix"); break; From a148d9994f1ef18268c64ade53625c480b97f5d9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 09:52:43 +0100 Subject: [PATCH 16/32] Split VectorSet* code into partial files --- .../Interfaces/IDatabase.VectorSets.cs | 244 ++++++++++++++ .../Interfaces/IDatabase.cs | 226 +------------ .../Interfaces/IDatabaseAsync.VectorSets.cs | 124 +++++++ .../Interfaces/IDatabaseAsync.cs | 99 +----- .../KeyPrefixed.VectorSets.cs | 90 +++++ .../KeyspaceIsolation/KeyPrefixed.cs | 83 +---- .../KeyPrefixedDatabase.VectorSets.cs | 87 +++++ .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 82 +---- .../RedisDatabase.VectorSets.cs | 313 ++++++++++++++++++ src/StackExchange.Redis/RedisDatabase.cs | 305 ----------------- 10 files changed, 862 insertions(+), 791 deletions(-) create mode 100644 src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs create mode 100644 src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs create mode 100644 src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs create mode 100644 src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs create mode 100644 src/StackExchange.Redis/RedisDatabase.VectorSets.cs diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs new file mode 100644 index 000000000..3872ba176 --- /dev/null +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -0,0 +1,244 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +/// +/// Describes functionality that is common to both standalone redis servers and redis clusters. +/// +public partial interface IDatabase +{ + // Vector Set operations + + /// + /// Add a vector to a vectorset. + /// + /// The key of the vectorset. + /// The element name. + /// The vector data. + /// Optional dimension reduction using random projection (REDUCE parameter). + /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. + /// Optional HNSW build exploration factor (EF parameter, default: 200). + /// Optional maximum connections per HNSW node (M parameter, default: 16). + /// Optional check-and-set mode for partial threading (CAS parameter). + /// Optional JSON attributes for the element (SETATTR parameter). + /// The flags to use for this operation. + /// if the element was added; if it already existed. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetAdd( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorSetQuantization quantization = VectorSetQuantization.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None); + + /// + /// Get the cardinality (number of elements) of a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// The cardinality of the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get the dimension of vectors in a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// The dimension of vectors in the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get the vector for a member. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The vector as a pooled memory lease. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetGetApproximateVector( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + /// Get JSON attributes for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The attributes as a JSON string. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get information about a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// Information about the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Check if a member exists in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// True if the member exists, false otherwise. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get links/connections for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The linked members. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get links/connections with scores for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The linked members with their similarity scores. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetGetLinksWithScores( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + /// Get a random member from a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// A random member from the vectorset, or null if the vectorset is empty. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get random members from a vectorset. + /// + /// The key of the vectorset. + /// The number of random members to return. + /// The flags to use for this operation. + /// Random members from the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Remove a member from a vectorset. + /// + /// The key of the vectorset. + /// The member to remove. + /// The flags to use for this operation. + /// if the member was removed; if it was not found. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Set JSON attributes for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The attributes to set as a JSON string. + /// The flags to use for this operation. + /// True if successful. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetSetAttributesJson( + RedisKey key, + RedisValue member, + string jsonAttributes, + CommandFlags flags = CommandFlags.None); + + /// + /// Find similar vectors using vector similarity search. + /// + /// The key of the vectorset. + /// The query vector. + /// The number of similar vectors to return (COUNT parameter). + /// Whether to include similarity scores in the results (WITHSCORES parameter). + /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). + /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). + /// Optional search exploration factor for better recall (EF parameter). + /// Optional filter expression to restrict results (FILTER parameter); . + /// Optional maximum filtering attempts (FILTER-EF parameter). + /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). + /// Whether to run search in main thread (NOTHREAD parameter). + /// The flags to use for this operation. + /// Similar vectors with their similarity scores. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetSimilaritySearchByVector( + RedisKey key, + ReadOnlyMemory vector, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); + + /// + /// Find similar vectors to an existing member. + /// + /// The key of the vectorset. + /// The member to find similar vectors for. + /// The number of similar vectors to return (COUNT parameter). + /// Whether to include similarity scores in the results (WITHSCORES parameter). + /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). + /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). + /// Optional search exploration factor for better recall (EF parameter). + /// Optional filter expression to restrict results (FILTER parameter). + /// Optional maximum filtering attempts (FILTER-EF parameter). + /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). + /// Whether to run search in main thread (NOTHREAD parameter). + /// The flags to use for this operation. + /// Similar vectors with their similarity scores. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetSimilaritySearchByMember( + RedisKey key, + RedisValue member, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 0d299f1b3..6c52e89bd 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Net; // ReSharper disable once CheckNamespace @@ -10,7 +9,7 @@ namespace StackExchange.Redis /// /// Describes functionality that is common to both standalone redis servers and redis clusters. /// - public interface IDatabase : IRedis, IDatabaseAsync + public partial interface IDatabase : IRedis, IDatabaseAsync { /// /// The numeric identifier of this database. @@ -3430,228 +3429,5 @@ IEnumerable SortedSetScan( /// The length of the string after it was modified by the command. /// RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); - - // Vector Set operations - - /// - /// Add a vector to a vectorset. - /// - /// The key of the vectorset. - /// The element name. - /// The vector data. - /// Optional dimension reduction using random projection (REDUCE parameter). - /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. - /// Optional HNSW build exploration factor (EF parameter, default: 200). - /// Optional maximum connections per HNSW node (M parameter, default: 16). - /// Optional check-and-set mode for partial threading (CAS parameter). - /// Optional JSON attributes for the element (SETATTR parameter). - /// The flags to use for this operation. - /// if the element was added; if it already existed. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - bool VectorSetAdd( - RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, - CommandFlags flags = CommandFlags.None); - - /// - /// Get the cardinality (number of elements) of a vectorset. - /// - /// The key of the vectorset. - /// The flags to use for this operation. - /// The cardinality of the vectorset. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - /// Get the dimension of vectors in a vectorset. - /// - /// The key of the vectorset. - /// The flags to use for this operation. - /// The dimension of vectors in the vectorset. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - /// Get the vector for a member. - /// - /// The key of the vectorset. - /// The member name. - /// The flags to use for this operation. - /// The vector as a pooled memory lease. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - /// Get JSON attributes for a member in a vectorset. - /// - /// The key of the vectorset. - /// The member name. - /// The flags to use for this operation. - /// The attributes as a JSON string. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - /// Get information about a vectorset. - /// - /// The key of the vectorset. - /// The flags to use for this operation. - /// Information about the vectorset. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - /// Check if a member exists in a vectorset. - /// - /// The key of the vectorset. - /// The member name. - /// The flags to use for this operation. - /// True if the member exists, false otherwise. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - /// Get links/connections for a member in a vectorset. - /// - /// The key of the vectorset. - /// The member name. - /// The flags to use for this operation. - /// The linked members. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - /// Get links/connections with scores for a member in a vectorset. - /// - /// The key of the vectorset. - /// The member name. - /// The flags to use for this operation. - /// The linked members with their similarity scores. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - /// Get a random member from a vectorset. - /// - /// The key of the vectorset. - /// The flags to use for this operation. - /// A random member from the vectorset, or null if the vectorset is empty. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - /// Get random members from a vectorset. - /// - /// The key of the vectorset. - /// The number of random members to return. - /// The flags to use for this operation. - /// Random members from the vectorset. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - - /// - /// Remove a member from a vectorset. - /// - /// The key of the vectorset. - /// The member to remove. - /// The flags to use for this operation. - /// if the member was removed; if it was not found. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - /// Set JSON attributes for a member in a vectorset. - /// - /// The key of the vectorset. - /// The member name. - /// The attributes to set as a JSON string. - /// The flags to use for this operation. - /// True if successful. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); - - /// - /// Find similar vectors using vector similarity search. - /// - /// The key of the vectorset. - /// The query vector. - /// The number of similar vectors to return (COUNT parameter). - /// Whether to include similarity scores in the results (WITHSCORES parameter). - /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). - /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). - /// Optional search exploration factor for better recall (EF parameter). - /// Optional filter expression to restrict results (FILTER parameter); . - /// Optional maximum filtering attempts (FILTER-EF parameter). - /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). - /// Whether to run search in main thread (NOTHREAD parameter). - /// The flags to use for this operation. - /// Similar vectors with their similarity scores. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetSimilaritySearchByVector( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None); - - /// - /// Find similar vectors to an existing member. - /// - /// The key of the vectorset. - /// The member to find similar vectors for. - /// The number of similar vectors to return (COUNT parameter). - /// Whether to include similarity scores in the results (WITHSCORES parameter). - /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). - /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). - /// Optional search exploration factor for better recall (EF parameter). - /// Optional filter expression to restrict results (FILTER parameter). - /// Optional maximum filtering attempts (FILTER-EF parameter). - /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). - /// Whether to run search in main thread (NOTHREAD parameter). - /// The flags to use for this operation. - /// Similar vectors with their similarity scores. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetSimilaritySearchByMember( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs new file mode 100644 index 000000000..8f4a6d511 --- /dev/null +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -0,0 +1,124 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +/// +/// Describes functionality that is common to both standalone redis servers and redis clusters. +/// +public partial interface IDatabaseAsync +{ + // Vector Set operations + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetAddAsync( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorSetQuantization quantization = VectorSetQuantization.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetGetApproximateVectorAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetGetAttributesJsonAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetGetLinksAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetGetLinksWithScoresAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetSetAttributesJsonAsync( + RedisKey key, + RedisValue member, + string jsonAttributes, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetSimilaritySearchByVectorAsync( + RedisKey key, + ReadOnlyMemory vector, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetSimilaritySearchByMemberAsync( + RedisKey key, + RedisValue member, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None); +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index fd6cb30d1..0bc7b4867 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading.Tasks; @@ -11,7 +10,7 @@ namespace StackExchange.Redis /// /// Describes functionality that is common to both standalone redis servers and redis clusters. /// - public interface IDatabaseAsync : IRedisAsync + public partial interface IDatabaseAsync : IRedisAsync { /// /// Indicates whether the instance can communicate with the server (resolved using the supplied key and optional flags). @@ -845,101 +844,5 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); - - // Vector Set operations - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetAddAsync( - RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, - CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetSimilaritySearchByVectorAsync( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetSimilaritySearchByMemberAsync( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs new file mode 100644 index 000000000..d6e7af940 --- /dev/null +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis.KeyspaceIsolation; + +internal partial class KeyPrefixed +{ + // Vector Set operations - async methods + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + public Task VectorSetAddAsync( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorSetQuantization quantization = VectorSetQuantization.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetAddAsync(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetLengthAsync(ToInner(key), flags); + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetDimensionAsync(ToInner(key), flags); + + public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetApproximateVectorAsync(ToInner(key), member, flags); + + public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetAttributesJsonAsync(ToInner(key), member, flags); + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetInfoAsync(ToInner(key), flags); + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetContainsAsync(ToInner(key), member, flags); + + public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksAsync(ToInner(key), member, flags); + + public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksWithScoresAsync(ToInner(key), member, flags); + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMemberAsync(ToInner(key), flags); + + public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMembersAsync(ToInner(key), count, flags); + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRemoveAsync(ToInner(key), member, flags); + + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, jsonAttributes, flags); + + public Task?> VectorSetSimilaritySearchByVectorAsync( + RedisKey key, + ReadOnlyMemory vector, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByVectorAsync(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + + public Task?> VectorSetSimilaritySearchByMemberAsync( + RedisKey key, + RedisValue member, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByMemberAsync(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); +} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 5ffb786c0..61a6f44c4 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync + internal partial class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync { internal KeyPrefixed(TInner inner, byte[] keyPrefix) { @@ -814,87 +814,6 @@ public void Wait(Task task) => public void WaitAll(params Task[] tasks) => Inner.WaitAll(tasks); - // Vector Set operations - async methods - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - public Task VectorSetAddAsync( - RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetAddAsync(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); - - public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetLengthAsync(ToInner(key), flags); - - public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetDimensionAsync(ToInner(key), flags); - - public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetApproximateVectorAsync(ToInner(key), member, flags); - - public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetAttributesJsonAsync(ToInner(key), member, flags); - - public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetInfoAsync(ToInner(key), flags); - - public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetContainsAsync(ToInner(key), member, flags); - - public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetLinksAsync(ToInner(key), member, flags); - - public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetLinksWithScoresAsync(ToInner(key), member, flags); - - public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetRandomMemberAsync(ToInner(key), flags); - - public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetRandomMembersAsync(ToInner(key), count, flags); - - public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetRemoveAsync(ToInner(key), member, flags); - - public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, jsonAttributes, flags); - - public Task?> VectorSetSimilaritySearchByVectorAsync( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByVectorAsync(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - - public Task?> VectorSetSimilaritySearchByMemberAsync( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByMemberAsync(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - protected internal RedisKey ToInner(RedisKey outer) => RedisKey.WithPrefix(Prefix, outer); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs new file mode 100644 index 000000000..21149c866 --- /dev/null +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs @@ -0,0 +1,87 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis.KeyspaceIsolation; + +internal sealed partial class KeyPrefixedDatabase +{ + // Vector Set operations + public bool VectorSetAdd( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorSetQuantization quantization = VectorSetQuantization.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetAdd(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetLength(ToInner(key), flags); + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetDimension(ToInner(key), flags); + + public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetApproximateVector(ToInner(key), member, flags); + + public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetAttributesJson(ToInner(key), member, flags); + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetInfo(ToInner(key), flags); + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetContains(ToInner(key), member, flags); + + public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinks(ToInner(key), member, flags); + + public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksWithScores(ToInner(key), member, flags); + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMember(ToInner(key), flags); + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMembers(ToInner(key), count, flags); + + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRemove(ToInner(key), member, flags); + + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJson(ToInner(key), member, jsonAttributes, flags); + + public Lease? VectorSetSimilaritySearchByVector( + RedisKey key, + ReadOnlyMemory vector, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByVector(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + + public Lease? VectorSetSimilaritySearchByMember( + RedisKey key, + RedisValue member, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchByMember(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); +} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index f01ab13fb..2a139694e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal sealed class KeyPrefixedDatabase : KeyPrefixed, IDatabase + internal sealed partial class KeyPrefixedDatabase : KeyPrefixed, IDatabase { public KeyPrefixedDatabase(IDatabase inner, byte[] prefix) : base(inner, prefix) { @@ -804,85 +804,5 @@ public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyTouch(ToInner(keys), flags); - - // Vector Set operations - public bool VectorSetAdd( - RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetAdd(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); - - public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetLength(ToInner(key), flags); - - public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetDimension(ToInner(key), flags); - - public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetApproximateVector(ToInner(key), member, flags); - - public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetAttributesJson(ToInner(key), member, flags); - - public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetInfo(ToInner(key), flags); - - public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetContains(ToInner(key), member, flags); - - public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetLinks(ToInner(key), member, flags); - - public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetGetLinksWithScores(ToInner(key), member, flags); - - public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetRandomMember(ToInner(key), flags); - - public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetRandomMembers(ToInner(key), count, flags); - - public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetRemove(ToInner(key), member, flags); - - public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSetAttributesJson(ToInner(key), member, jsonAttributes, flags); - - public Lease? VectorSetSimilaritySearchByVector( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByVector(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - - public Lease? VectorSetSimilaritySearchByMember( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByMember(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); } } diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs new file mode 100644 index 000000000..cc5a8bd65 --- /dev/null +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -0,0 +1,313 @@ +using System; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +internal partial class RedisDatabase +{ + public bool VectorSetAdd( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorSetQuantization quantization = VectorSetQuantization.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) + { + var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); + return ExecuteSync(msg, ResultProcessor.Int32); + } + + public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); + return ExecuteSync(msg, ResultProcessor.LeaseFloat32); + } + + public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); + return ExecuteSync(msg, ResultProcessor.String); + } + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); + return ExecuteSync(msg, ResultProcessor.VectorSetInfo); + } + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); + return ExecuteSync(msg, ResultProcessor.VectorSetLinks); + } + + public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); + return ExecuteSync(msg, ResultProcessor.VectorSetLinksWithScores); + } + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public Lease? VectorSetSimilaritySearchByVector( + RedisKey key, + ReadOnlyMemory vector, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + RedisValue.Null, + vector, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteSync(msg, msg.GetResultProcessor()); + } + + public Lease? VectorSetSimilaritySearchByMember( + RedisKey key, + RedisValue member, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + member, + ReadOnlyMemory.Empty, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteSync(msg, msg.GetResultProcessor()); + } + + // Vector Set async operations + public Task VectorSetAddAsync( + RedisKey key, + RedisValue element, + ReadOnlyMemory values, + int? reducedDimensions = null, + VectorSetQuantization quantization = VectorSetQuantization.Int8, + int? buildExplorationFactor = null, + int? maxConnections = null, + bool useCheckAndSet = false, + string? attributesJson = null, + CommandFlags flags = CommandFlags.None) + { + var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); + return ExecuteAsync(msg, ResultProcessor.Int32); + } + + public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); + return ExecuteAsync(msg, ResultProcessor.LeaseFloat32); + } + + public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); + return ExecuteAsync(msg, ResultProcessor.String); + } + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); + return ExecuteAsync(msg, ResultProcessor.VectorSetInfo); + } + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); + return ExecuteAsync(msg, ResultProcessor.VectorSetLinks); + } + + public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); + return ExecuteAsync(msg, ResultProcessor.VectorSetLinksWithScores); + } + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task?> VectorSetSimilaritySearchByVectorAsync( + RedisKey key, + ReadOnlyMemory vector, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + RedisValue.Null, + vector, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteAsync(msg, msg.GetResultProcessor()); + } + + public Task?> VectorSetSimilaritySearchByMemberAsync( + RedisKey key, + RedisValue member, + int? count = null, + bool withScores = false, + bool withAttributes = false, + double? epsilon = null, + int? searchExplorationFactor = null, + string? filterExpression = null, + int? maxFilteringEffort = null, + bool useExactSearch = false, + bool disableThreading = false, + CommandFlags flags = CommandFlags.None) + { + var msg = new VectorSetSimilaritySearchMessage( + Database, + flags, + key, + member, + ReadOnlyMemory.Empty, + count, + withScores, + withAttributes, + epsilon, + searchExplorationFactor, + filterExpression, + maxFilteringEffort, + useExactSearch, + disableThreading); + return ExecuteAsync(msg, msg.GetResultProcessor()); + } +} diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 22fe7711c..bf69f25f3 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5721,310 +5721,5 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } } - - public bool VectorSetAdd( - RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); - return ExecuteSync(msg, ResultProcessor.Boolean); - } - - public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); - return ExecuteSync(msg, ResultProcessor.Int64); - } - - public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); - return ExecuteSync(msg, ResultProcessor.Int32); - } - - public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); - return ExecuteSync(msg, ResultProcessor.LeaseFloat32); - } - - public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); - return ExecuteSync(msg, ResultProcessor.String); - } - - public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); - return ExecuteSync(msg, ResultProcessor.VectorSetInfo); - } - - public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); - return ExecuteSync(msg, ResultProcessor.Boolean); - } - - public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); - return ExecuteSync(msg, ResultProcessor.VectorSetLinks); - } - - public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); - return ExecuteSync(msg, ResultProcessor.VectorSetLinksWithScores); - } - - public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); - return ExecuteSync(msg, ResultProcessor.RedisValue); - } - - public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); - return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); - } - - public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); - return ExecuteSync(msg, ResultProcessor.Boolean); - } - - public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); - return ExecuteSync(msg, ResultProcessor.Boolean); - } - - public Lease? VectorSetSimilaritySearchByVector( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - RedisValue.Null, - vector, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); - return ExecuteSync(msg, msg.GetResultProcessor()); - } - - public Lease? VectorSetSimilaritySearchByMember( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - member, - ReadOnlyMemory.Empty, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); - return ExecuteSync(msg, msg.GetResultProcessor()); - } - - // Vector Set async operations - public Task VectorSetAddAsync( - RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); - return ExecuteAsync(msg, ResultProcessor.Boolean); - } - - public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); - return ExecuteAsync(msg, ResultProcessor.Int64); - } - - public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); - return ExecuteAsync(msg, ResultProcessor.Int32); - } - - public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); - return ExecuteAsync(msg, ResultProcessor.LeaseFloat32); - } - - public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); - return ExecuteAsync(msg, ResultProcessor.String); - } - - public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); - return ExecuteAsync(msg, ResultProcessor.VectorSetInfo); - } - - public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); - return ExecuteAsync(msg, ResultProcessor.Boolean); - } - - public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); - return ExecuteAsync(msg, ResultProcessor.VectorSetLinks); - } - - public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); - return ExecuteAsync(msg, ResultProcessor.VectorSetLinksWithScores); - } - - public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); - return ExecuteAsync(msg, ResultProcessor.RedisValue); - } - - public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); - } - - public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); - return ExecuteAsync(msg, ResultProcessor.Boolean); - } - - public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) - { - var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); - return ExecuteAsync(msg, ResultProcessor.Boolean); - } - - public Task?> VectorSetSimilaritySearchByVectorAsync( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - RedisValue.Null, - vector, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); - return ExecuteAsync(msg, msg.GetResultProcessor()); - } - - public Task?> VectorSetSimilaritySearchByMemberAsync( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - member, - ReadOnlyMemory.Empty, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); - return ExecuteAsync(msg, msg.GetResultProcessor()); - } } } From 337cbba7c38324b92df03fd4650a8207ed7112a1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 10:09:39 +0100 Subject: [PATCH 17/32] Split VectorSet code from ResultProcessor (also: *Lease*) --- .../ResultProcessor.Lease.cs | 218 ++++++++++++ .../ResultProcessor.VectorSets.cs | 112 +++++++ src/StackExchange.Redis/ResultProcessor.cs | 314 +----------------- 3 files changed, 331 insertions(+), 313 deletions(-) create mode 100644 src/StackExchange.Redis/ResultProcessor.Lease.cs create mode 100644 src/StackExchange.Redis/ResultProcessor.VectorSets.cs diff --git a/src/StackExchange.Redis/ResultProcessor.Lease.cs b/src/StackExchange.Redis/ResultProcessor.Lease.cs new file mode 100644 index 000000000..c0f9e6d8e --- /dev/null +++ b/src/StackExchange.Redis/ResultProcessor.Lease.cs @@ -0,0 +1,218 @@ +using System.Diagnostics; +using Pipelines.Sockets.Unofficial.Arenas; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +internal abstract partial class ResultProcessor +{ + // Lease result processors + public static readonly ResultProcessor?> LeaseFloat32 = new LeaseFloat32Processor(); + + public static readonly ResultProcessor> + Lease = new LeaseProcessor(); + + public static readonly ResultProcessor> + LeaseFromArray = new LeaseFromArrayProcessor(); + + private abstract class LeaseProcessor : ResultProcessor?> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + + // deal with null + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + + // lease and fill + var items = result.GetItems(); + var length = checked((int)items.Length); + var lease = Lease.Create(length, clear: false); // note this handles zero nicely + var target = lease.Span; + int index = 0; + foreach (ref RawResult item in items) + { + if (!TryParse(item, out target[index++])) + { + // something went wrong; recycle and quit + lease.Dispose(); + return false; + } + } + Debug.Assert(index == length, "length mismatch"); + SetResult(message, lease); + return true; + } + + protected abstract bool TryParse(in RawResult raw, out T parsed); + } + + private abstract class InterleavedLeaseProcessor : ResultProcessor?> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + + // deal with null + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + + // lease and fill + var items = result.GetItems(); + var length = checked((int)items.Length) / 2; + var lease = Lease.Create(length, clear: false); // note this handles zero nicely + var target = lease.Span; + + var iter = items.GetEnumerator(); + for (int i = 0; i < target.Length; i++) + { + bool ok = iter.MoveNext(); + if (ok) + { + ref readonly RawResult first = ref iter.Current; + ok = iter.MoveNext() && TryParse(in first, in iter.Current, out target[i]); + } + if (!ok) + { + lease.Dispose(); + return false; + } + } + SetResult(message, lease); + return true; + } + + protected abstract bool TryParse(in RawResult first, in RawResult second, out T parsed); + } + + // takes a nested vector of the form [[A],[B,C],[D]] and exposes it as [A,B,C,D]; this is + // especially useful for VLINKS + private abstract class FlattenedLeaseProcessor : ResultProcessor?> + { + protected virtual long GetArrayLength(in RawResult array) => array.GetItems().Length; + + protected virtual bool TryReadOne(ref Sequence.Enumerator reader, out T value) + { + if (reader.MoveNext()) + { + return TryReadOne(in reader.Current, out value); + } + value = default!; + return false; + } + + protected virtual bool TryReadOne(in RawResult result, out T value) + { + value = default!; + return false; + } + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + var items = result.GetItems(); + long length = 0; + foreach (ref RawResult item in items) + { + if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + { + length += GetArrayLength(in item); + } + } + + if (length == 0) + { + SetResult(message, Lease.Empty); + return true; + } + var lease = Lease.Create(checked((int)length), clear: false); + int index = 0; + var target = lease.Span; + foreach (ref RawResult item in items) + { + if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + { + var iter = item.GetItems().GetEnumerator(); + while (index < target.Length && TryReadOne(ref iter, out target[index])) + { + index++; + } + } + } + + if (index == length) + { + SetResult(message, lease); + return true; + } + lease.Dispose(); // failed to fill? + return false; + } + } + + private sealed class LeaseFloat32Processor : LeaseProcessor + { + protected override bool TryParse(in RawResult raw, out float parsed) + { + var result = raw.TryGetDouble(out double val); + parsed = (float)val; + return result; + } + } + + private sealed class LeaseProcessor : ResultProcessor> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + SetResult(message, result.AsLease()!); + return true; + } + return false; + } + } + + private sealed class LeaseFromArrayProcessor : ResultProcessor> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Array: + var items = result.GetItems(); + if (items.Length == 1) + { // treat an array of 1 like a single reply + SetResult(message, items[0].AsLease()!); + return true; + } + break; + } + return false; + } + } +} diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs new file mode 100644 index 000000000..320899dc2 --- /dev/null +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -0,0 +1,112 @@ +using Pipelines.Sockets.Unofficial.Arenas; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +internal abstract partial class ResultProcessor +{ + // VectorSet result processors + public static readonly ResultProcessor?> VectorSetLinksWithScores = new VectorSetLinksWithScoresProcessor(); + public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); + + public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); + + private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor + { + protected override long GetArrayLength(in RawResult array) => array.GetItems().Length / 2; + + protected override bool TryReadOne(ref Sequence.Enumerator reader, out VectorSetLink value) + { + if (reader.MoveNext()) + { + ref readonly RawResult first = ref reader.Current; + if (reader.MoveNext() && reader.Current.TryGetDouble(out var score)) + { + value = new VectorSetLink(first.AsRedisValue(), score); + return true; + } + } + value = default; + return false; + } + } + + private sealed class VectorSetLinksProcessor : FlattenedLeaseProcessor + { + protected override bool TryReadOne(in RawResult result, out RedisValue value) + { + value = result.AsRedisValue(); + return true; + } + } + + private sealed class VectorSetInfoProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array) + { + if (result.IsNull) + { + SetResult(message, null); + return true; + } + var quantType = VectorSetQuantization.Unknown; + string? quantTypeRaw = null; + int vectorDim = 0, maxLevel = 0; + long size = 0, vsetUid = 0, hnswMaxNodeUid = 0; + var iter = result.GetItems().GetEnumerator(); + while (iter.MoveNext()) + { + var key = iter.Current; + if (!iter.MoveNext()) break; + var value = iter.Current; + + var len = key.Payload.Length; + var keyHash = key.Payload.Hash64(); + switch (key.Payload.Length) + { + case 4 when keyHash == FastHash._4.size && key.IsEqual(FastHash._4.size_u8) && value.TryGetInt64(out var i64): + size = i64; + break; + case 8 when keyHash == FastHash._8.vset_uid && key.IsEqual(FastHash._8.vset_uid_u8) && value.TryGetInt64(out var i64): + vsetUid = i64; + break; + case 9 when keyHash == FastHash._9.max_level && key.IsEqual(FastHash._9.max_level_u8) && value.TryGetInt64(out var i64): + maxLevel = checked((int)i64); + break; + case 10 when keyHash == FastHash._10.vector_dim && key.IsEqual(FastHash._10.vector_dim_u8) && value.TryGetInt64(out var i64): + vectorDim = checked((int)i64); + break; + case 10 when keyHash == FastHash._10.quant_type && key.IsEqual(FastHash._10.quant_type_u8): + var qHash = value.Payload.Hash64(); + switch (value.Payload.Length) + { + case 3 when qHash == FastHash._3.bin && value.IsEqual(FastHash._3.bin_u8): + quantType = VectorSetQuantization.Binary; + break; + case 3 when qHash == FastHash._3.f32 && value.IsEqual(FastHash._3.f32_u8): + quantType = VectorSetQuantization.None; + break; + case 4 when qHash == FastHash._4.int8 && value.IsEqual(FastHash._4.int8_u8): + quantType = VectorSetQuantization.Int8; + break; + default: + quantTypeRaw = value.GetString(); + quantType = VectorSetQuantization.Unknown; + break; + } + break; + case 17 when keyHash == FastHash._17.hnsw_max_node_uid && key.IsEqual(FastHash._17.hnsw_max_node_uid_u8) && value.TryGetInt64(out var i64): + hnswMaxNodeUid = i64; + break; + } + } + + SetResult(message, new VectorSetInfo(quantType, quantTypeRaw, vectorDim, size, maxLevel, vsetUid, hnswMaxNodeUid)); + return true; + } + return false; + } + } +} diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 8bf55b04b..650cba603 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -14,7 +14,7 @@ namespace StackExchange.Redis { - internal abstract class ResultProcessor + internal abstract partial class ResultProcessor { public static readonly ResultProcessor Boolean = new BooleanProcessor(), @@ -61,12 +61,6 @@ public static readonly ResultProcessor Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1); public static readonly ResultProcessor Int32 = new Int32Processor(); - public static readonly ResultProcessor?> LeaseFloat32 = new LeaseFloat32Processor(); - - public static readonly ResultProcessor?> VectorSetLinksWithScores = new VectorSetLinksWithScoresProcessor(); - public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); - - public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -99,12 +93,6 @@ public static readonly ResultProcessor public static readonly ResultProcessor RedisValueFromArray = new RedisValueFromArrayProcessor(); - public static readonly ResultProcessor> - Lease = new LeaseProcessor(); - - public static readonly ResultProcessor> - LeaseFromArray = new LeaseFromArrayProcessor(); - public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); @@ -1712,271 +1700,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private abstract class LeaseProcessor : ResultProcessor?> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - if (result.Resp2TypeArray != ResultType.Array) - { - return false; // not an array - } - - // deal with null - if (result.IsNull) - { - SetResult(message, Lease.Empty); - return true; - } - - // lease and fill - var items = result.GetItems(); - var length = checked((int)items.Length); - var lease = Lease.Create(length, clear: false); // note this handles zero nicely - var target = lease.Span; - int index = 0; - foreach (ref RawResult item in items) - { - if (!TryParse(item, out target[index++])) - { - // something went wrong; recycle and quit - lease.Dispose(); - return false; - } - } - Debug.Assert(index == length, "length mismatch"); - SetResult(message, lease); - return true; - } - - protected abstract bool TryParse(in RawResult raw, out T parsed); - } - - private abstract class InterleavedLeaseProcessor : ResultProcessor?> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - if (result.Resp2TypeArray != ResultType.Array) - { - return false; // not an array - } - - // deal with null - if (result.IsNull) - { - SetResult(message, Lease.Empty); - return true; - } - - // lease and fill - var items = result.GetItems(); - var length = checked((int)items.Length) / 2; - var lease = Lease.Create(length, clear: false); // note this handles zero nicely - var target = lease.Span; - - var iter = items.GetEnumerator(); - for (int i = 0; i < target.Length; i++) - { - bool ok = iter.MoveNext(); - if (ok) - { - ref readonly RawResult first = ref iter.Current; - ok = iter.MoveNext() && TryParse(in first, in iter.Current, out target[i]); - } - if (!ok) - { - lease.Dispose(); - return false; - } - } - SetResult(message, lease); - return true; - } - - protected abstract bool TryParse(in RawResult first, in RawResult second, out T parsed); - } - - // takes a nested vector of the form [[A],[B,C],[D]] and exposes it as [A,B,C,D]; this is - // especially useful for VLINKS - private abstract class FlattenedLeaseProcessor : ResultProcessor?> - { - protected virtual long GetArrayLength(in RawResult array) => array.GetItems().Length; - - protected virtual bool TryReadOne(ref Sequence.Enumerator reader, out T value) - { - if (reader.MoveNext() && TryReadOne(reader.Current, out value)) - { - return true; - } - value = default!; - return false; - } - - protected virtual bool TryReadOne(in RawResult result, out T value) - { - value = default!; - return false; - } - - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - if (result.Resp2TypeArray != ResultType.Array) - { - return false; // not an array - } - if (result.IsNull) - { - SetResult(message, Lease.Empty); - return true; - } - var items = result.GetItems(); - long length = 0; - foreach (ref RawResult item in items) - { - if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) - { - length += GetArrayLength(item); - } - } - - if (length == 0) - { - SetResult(message, Lease.Empty); - return true; - } - var lease = Lease.Create(checked((int)length), clear: false); - int index = 0; - var target = lease.Span; - foreach (ref RawResult item in items) - { - if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) - { - var iter = item.GetItems().GetEnumerator(); - while (index < target.Length && TryReadOne(ref iter, out T value)) - { - target[index++] = value; - } - } - } - - if (index == length) - { - SetResult(message, lease); - return true; - } - lease.Dispose(); // failed to fill? - return false; - } - } - - private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor - { - protected override long GetArrayLength(in RawResult array) => array.GetItems().Length / 2; - - protected override bool TryReadOne(ref Sequence.Enumerator reader, out VectorSetLink value) - { - if (reader.MoveNext()) - { - ref readonly RawResult first = ref reader.Current; - if (reader.MoveNext() && reader.Current.TryGetDouble(out var score)) - { - value = new VectorSetLink(first.AsRedisValue(), score); - return true; - } - } - value = default; - return false; - } - } - - private sealed class VectorSetLinksProcessor : FlattenedLeaseProcessor - { - protected override bool TryReadOne(in RawResult result, out RedisValue value) - { - value = result.AsRedisValue(); - return true; - } - } - - private sealed class VectorSetInfoProcessor : ResultProcessor - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - if (result.Resp2TypeArray == ResultType.Array) - { - if (result.IsNull) - { - SetResult(message, null); - return true; - } - var quantType = VectorSetQuantization.Unknown; - string? quantTypeRaw = null; - int vectorDim = 0, maxLevel = 0; - long size = 0, vsetUid = 0, hnswMaxNodeUid = 0; - var iter = result.GetItems().GetEnumerator(); - while (iter.MoveNext()) - { - var key = iter.Current; - if (!iter.MoveNext()) break; - var value = iter.Current; - - var len = key.Payload.Length; - var keyHash = key.Payload.Hash64(); - switch (key.Payload.Length) - { - case 4 when keyHash == FastHash._4.size && key.IsEqual(FastHash._4.size_u8) && value.TryGetInt64(out var i64): - size = i64; - break; - case 8 when keyHash == FastHash._8.vset_uid && key.IsEqual(FastHash._8.vset_uid_u8) && value.TryGetInt64(out var i64): - vsetUid = i64; - break; - case 9 when keyHash == FastHash._9.max_level && key.IsEqual(FastHash._9.max_level_u8) && value.TryGetInt64(out var i64): - maxLevel = checked((int)i64); - break; - case 10 when keyHash == FastHash._10.vector_dim && key.IsEqual(FastHash._10.vector_dim_u8) && value.TryGetInt64(out var i64): - vectorDim = checked((int)i64); - break; - case 10 when keyHash == FastHash._10.quant_type && key.IsEqual(FastHash._10.quant_type_u8): - var qHash = value.Payload.Hash64(); - switch (value.Payload.Length) - { - case 3 when qHash == FastHash._3.bin && value.IsEqual(FastHash._3.bin_u8): - quantType = VectorSetQuantization.Binary; - break; - case 3 when qHash == FastHash._3.f32 && value.IsEqual(FastHash._3.f32_u8): - quantType = VectorSetQuantization.None; - break; - case 4 when qHash == FastHash._4.int8 && value.IsEqual(FastHash._4.int8_u8): - quantType = VectorSetQuantization.Int8; - break; - default: - quantTypeRaw = value.GetString(); - quantType = VectorSetQuantization.Unknown; - break; - } - break; - case 17 when keyHash == FastHash._17.hnsw_max_node_uid && key.IsEqual(FastHash._17.hnsw_max_node_uid_u8) && value.TryGetInt64(out var i64): - hnswMaxNodeUid = i64; - break; - } - } - - SetResult(message, new VectorSetInfo(quantType, quantTypeRaw, vectorDim, size, maxLevel, vsetUid, hnswMaxNodeUid)); - return true; - } - return false; - } - } - - private sealed class LeaseFloat32Processor : LeaseProcessor - { - protected override bool TryParse(in RawResult raw, out float parsed) - { - var result = raw.TryGetDouble(out double val); - parsed = (float)val; - return result; - } - } - private sealed class Int64ArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -2351,41 +2074,6 @@ private static bool TryParsePrimaryReplica(in Sequence items, out Rol } } - private sealed class LeaseProcessor : ResultProcessor> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - switch (result.Resp2TypeBulkString) - { - case ResultType.Integer: - case ResultType.SimpleString: - case ResultType.BulkString: - SetResult(message, result.AsLease()!); - return true; - } - return false; - } - } - - private sealed class LeaseFromArrayProcessor : ResultProcessor> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - switch (result.Resp2TypeBulkString) - { - case ResultType.Array: - var items = result.GetItems(); - if (items.Length == 1) - { // treat an array of 1 like a single reply - SetResult(message, items[0].AsLease()!); - return true; - } - break; - } - return false; - } - } - private sealed class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) From d82c9a47be9ef925112f1a8292bf819a81da369e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 10:17:44 +0100 Subject: [PATCH 18/32] key can be embstr or raw --- tests/StackExchange.Redis.Tests/KeyTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index 31cd87d79..b82aeda05 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -182,8 +182,8 @@ public async Task KeyEncoding() db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - Assert.Equal("embstr", db.KeyEncoding(key)); - Assert.Equal("embstr", await db.KeyEncodingAsync(key)); + Assert.True(db.KeyEncoding(key) is "embstr" or "raw"); + Assert.True((await db.KeyEncodingAsync(key)) is "embstr" or "raw"); db.KeyDelete(key, CommandFlags.FireAndForget); db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); From 63201857b9f957a547996725b3865ad18ae21dad Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 16:28:41 +0100 Subject: [PATCH 19/32] Use code-generator for `[FastHash]`. --- Directory.Packages.props | 5 + StackExchange.Redis.sln | 9 + .../FastHashGenerator.cs | 174 ++++++++++++++++++ .../FastHashGenerator.md | 58 ++++++ .../StackExchange.Redis.Build.csproj | 20 ++ src/StackExchange.Redis/FastHash.Literals.cs | 63 ++----- src/StackExchange.Redis/FastHash.cs | 4 +- .../ResultProcessor.VectorSets.cs | 37 ++-- .../StackExchange.Redis.csproj | 4 + .../FastHashTests.cs | 115 ------------ 10 files changed, 309 insertions(+), 180 deletions(-) create mode 100644 eng/StackExchange.Redis.Build/FastHashGenerator.cs create mode 100644 eng/StackExchange.Redis.Build/FastHashGenerator.md create mode 100644 eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj delete mode 100644 tests/StackExchange.Redis.Tests/FastHashTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 79c404dc2..df8c078a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,10 @@ + + + + @@ -23,6 +27,7 @@ + diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 8f772ae42..2ed4ebfb3 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -122,6 +122,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{5FA0958E-6EBD-45F4-808E-3447A293F96F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{190742E1-FA50-4E36-A8C4-88AE87654340}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -180,6 +184,10 @@ Global {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.Build.0 = Debug|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.ActiveCfg = Release|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +210,7 @@ Global {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} + {190742E1-FA50-4E36-A8C4-88AE87654340} = {5FA0958E-6EBD-45F4-808E-3447A293F96F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs new file mode 100644 index 000000000..a5add2af5 --- /dev/null +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.cs @@ -0,0 +1,174 @@ +using System.Buffers; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StackExchange.Redis.Build; + +[Generator(LanguageNames.CSharp)] +public class FastHashGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var literals = context.SyntaxProvider + .CreateSyntaxProvider(Predicate, Transform) + .Where(pair => pair.Name is { Length: > 0 }) + .Collect(); + + context.RegisterSourceOutput(literals, Generate); + } + + private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) + { + // looking for [FastHash] partial static class Foo { } + if (node is ClassDeclarationSyntax decl + && decl.Modifiers.Any(SyntaxKind.StaticKeyword) + && decl.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + foreach (var attribList in decl.AttributeLists) + { + foreach (var attrib in attribList.Attributes) + { + if (attrib.Name.ToString() is "FastHashAttribute" or "FastHash") return true; + } + } + } + + return false; + } + + private (string Namespace, string ParentType, string Name, string Value) Transform( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) + { + // extract the name and value (defaults to name, but can be overridden via attribute) and the location + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol named) return default; + string ns = "", parentType = ""; + if (named.ContainingType is { } containingType) + { + parentType = containingType.Name; // don't worry about multi-level nesting for now; add later if needed + ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + else if (named.ContainingNamespace is { } containingNamespace) + { + ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + + string name = named.Name, value = ""; + foreach (var attrib in named.GetAttributes()) + { + if (attrib.AttributeClass?.Name == "FastHashAttribute") + { + if (attrib.ConstructorArguments.Length == 1) + { + if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + { + value = val; + break; + } + } + } + } + + if (string.IsNullOrWhiteSpace(value)) + { + value = name.Replace("_", "-"); // if nothing explicit: infer from name + } + + return (ns, parentType, name, value); + } + + private string GetVersion() + { + var asm = GetType().Assembly; + if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is + AssemblyFileVersionAttribute { Version: { Length: > 0 } } version) + { + return version.Version; + } + + return asm.GetName().Version?.ToString() ?? "??"; + } + + private void Generate( + SourceProductionContext ctx, + ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> literals) + { + if (literals.IsDefaultOrEmpty) return; + + var sb = new StringBuilder("// ") + .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine(); + + // lease a buffer that is big enough for the longest string + var buffer = ArrayPool.Shared.Rent( + Encoding.UTF8.GetMaxByteCount(literals.Max(l => l.Value.Length))); + int indent = 0; + + StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); + NewLine().Append("using System;"); + NewLine().Append("using StackExchange.Redis;"); + NewLine().Append("#pragma warning disable CS8981"); + foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType))) + { + NewLine(); + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + } + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + } + + foreach (var literal in grp) + { + int len; + unsafe + { + fixed (byte* bPtr = buffer) // netstandard2.0 forces fallback API + { + fixed (char* cPtr = literal.Value) + { + len = Encoding.UTF8.GetBytes(cPtr, literal.Value.Length, bPtr, buffer.Length); + } + } + } + + var hash = FastHash.Hash64(buffer.AsSpan(0, len)); + NewLine().Append("static partial class ").Append(literal.Name); + NewLine().Append("{"); + indent++; + NewLine().Append("public const int Length = ").Append(len).Append(';'); + NewLine().Append("public const long Hash = ").Append(hash).Append(';'); + NewLine().Append("public static ReadOnlySpan U8 => @\"") + .Append(literal.Value.Replace("\"", "\"\"")).Append("\"u8;"); + NewLine().Append("public static string Text => @\"") + .Append(literal.Value.Replace("\"", "\"\"")).Append("\";"); + NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);"); + indent--; + NewLine().Append("}"); + } + + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + indent--; + NewLine().Append("}"); + } + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + indent--; + NewLine().Append("}"); + } + } + + ArrayPool.Shared.Return(buffer); + ctx.AddSource("FastHash.generated.cs", sb.ToString()); + } +} diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md new file mode 100644 index 000000000..2a7408ed8 --- /dev/null +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.md @@ -0,0 +1,58 @@ +# FastHashGenerator + +Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals. + +The purpose of this generator is to interpret inputs like: + +``` c# +[FastHash] public static partial class bin { } +[FastHash] public static partial class f32 { } +``` + +Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier. +Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`. +The generator demands *all* of `[FastHash] public static partial class`. + +The output is of the form: + +``` c# +static partial class bin +{ + public const int Length = 3; + public const long Hash = 7235938; + public static ReadOnlySpan U8 => @"bin"u8; + public static string Text => @"bin"; + public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8); +} +static partial class f32 +{ + public const int Length = 3; + public const long Hash = 3289958; + public static ReadOnlySpan U8 => @"f32"u8; + public static string Text => @"f32"; + public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8); +} +``` + +This allows for fast, efficient, and safe matching of well-known tokens, for example: + +``` c# +var key = ... +var hash = key.Hash64(); +switch (key.Length) +{ + case bin.Length when bin.Is(hash, key): + // handle bin + break; + case f32.Length when f32.Is(hash, key): + // handle f32 + break; +} +``` + +The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) +as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches +must also perform a sequence equality check - the `Is` convenient method validates both hash and equality. + + + diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj new file mode 100644 index 000000000..f875133ba --- /dev/null +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + enable + enable + true + + + + + + + + + FastHash.cs + + + + diff --git a/src/StackExchange.Redis/FastHash.Literals.cs b/src/StackExchange.Redis/FastHash.Literals.cs index b5e18bfd0..44cb281ba 100644 --- a/src/StackExchange.Redis/FastHash.Literals.cs +++ b/src/StackExchange.Redis/FastHash.Literals.cs @@ -1,57 +1,20 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; namespace StackExchange.Redis; -// See FastHashTests for how these are validated and enforced. When adding new values, use any -// value and run the tests - this will tell you the correct value. [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "To better represent the expected literals")] internal static partial class FastHash { -#pragma warning disable SA1300, SA1303 - public static class _3 - { - public const long bin = 7235938; - public static ReadOnlySpan bin_u8 => "bin"u8; - - public const long f32 = 3289958; - public static ReadOnlySpan f32_u8 => "f32"u8; - } - - public static class _4 - { - public const long size = 1702521203; - public static ReadOnlySpan size_u8 => "size"u8; - - public const long int8 = 947154537; - public static ReadOnlySpan int8_u8 => "int8"u8; - } - - public static class _8 - { - public const long vset_uid = 7235443114434196342; - public static ReadOnlySpan vset_uid_u8 => "vset-uid"u8; - } - - public static class _9 - { - public const long max_level = 7311142560376316269; - public static ReadOnlySpan max_level_u8 => "max-level"u8; - } - - public static class _10 - { - public const long quant_type = 8751669953979053425; - public static ReadOnlySpan quant_type_u8 => "quant-type"u8; - - public const long vector_dim = 7218551600764380534; - public static ReadOnlySpan vector_dim_u8 => "vector-dim"u8; - } - - public static class _17 - { - public const long hnsw_max_node_uid = 8674334399337295464; - public static ReadOnlySpan hnsw_max_node_uid_u8 => "hnsw-max-node-uid"u8; - } -#pragma warning restore SA1300, SA1303 + // see HastHashGenerator.md for more information and intended usage. +#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 + [FastHash] public static partial class bin { } + [FastHash] public static partial class f32 { } + [FastHash] public static partial class int8 { } + [FastHash] public static partial class size { } + [FastHash] public static partial class vset_uid { } + [FastHash] public static partial class max_level { } + [FastHash] public static partial class quant_type { } + [FastHash] public static partial class vector_dim { } + [FastHash] public static partial class hnsw_max_node_uid { } +#pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 } diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs index 5b26b521c..f8b9e5ad4 100644 --- a/src/StackExchange.Redis/FastHash.cs +++ b/src/StackExchange.Redis/FastHash.cs @@ -28,8 +28,8 @@ to construct strings when parsing tokens. */ internal static partial class FastHash { - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] - public sealed class LiteralAttribute(string token) : Attribute + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class FastHashAttribute(string token = "") : Attribute { public string Token => token; } diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index 320899dc2..2383f33d1 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -1,4 +1,5 @@ using Pipelines.Sockets.Unofficial.Arenas; +using FH = global::StackExchange.Redis.FastHash; // ReSharper disable once CheckNamespace namespace StackExchange.Redis; @@ -6,7 +7,9 @@ namespace StackExchange.Redis; internal abstract partial class ResultProcessor { // VectorSet result processors - public static readonly ResultProcessor?> VectorSetLinksWithScores = new VectorSetLinksWithScoresProcessor(); + public static readonly ResultProcessor?> VectorSetLinksWithScores = + new VectorSetLinksWithScoresProcessor(); + public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); @@ -26,6 +29,7 @@ protected override bool TryReadOne(ref Sequence.Enumerator reader, ou return true; } } + value = default; return false; } @@ -51,6 +55,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, null); return true; } + var quantType = VectorSetQuantization.Unknown; string? quantTypeRaw = null; int vectorDim = 0, maxLevel = 0; @@ -58,37 +63,38 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var iter = result.GetItems().GetEnumerator(); while (iter.MoveNext()) { - var key = iter.Current; + ref readonly RawResult key = ref iter.Current; if (!iter.MoveNext()) break; - var value = iter.Current; + ref readonly RawResult value = ref iter.Current; var len = key.Payload.Length; var keyHash = key.Payload.Hash64(); switch (key.Payload.Length) { - case 4 when keyHash == FastHash._4.size && key.IsEqual(FastHash._4.size_u8) && value.TryGetInt64(out var i64): + case FH.size.Length when FH.size.Is(keyHash, key) && value.TryGetInt64(out var i64): size = i64; break; - case 8 when keyHash == FastHash._8.vset_uid && key.IsEqual(FastHash._8.vset_uid_u8) && value.TryGetInt64(out var i64): + case FH.vset_uid.Length when FH.vset_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): vsetUid = i64; break; - case 9 when keyHash == FastHash._9.max_level && key.IsEqual(FastHash._9.max_level_u8) && value.TryGetInt64(out var i64): + case FH.max_level.Length when FH.max_level.Is(keyHash, key) && value.TryGetInt64(out var i64): maxLevel = checked((int)i64); break; - case 10 when keyHash == FastHash._10.vector_dim && key.IsEqual(FastHash._10.vector_dim_u8) && value.TryGetInt64(out var i64): + case FH.vector_dim.Length + when FH.vector_dim.Is(keyHash, key) && value.TryGetInt64(out var i64): vectorDim = checked((int)i64); break; - case 10 when keyHash == FastHash._10.quant_type && key.IsEqual(FastHash._10.quant_type_u8): + case FH.quant_type.Length when FH.quant_type.Is(keyHash, key): var qHash = value.Payload.Hash64(); switch (value.Payload.Length) { - case 3 when qHash == FastHash._3.bin && value.IsEqual(FastHash._3.bin_u8): + case FH.bin.Length when FH.bin.Is(qHash, value): quantType = VectorSetQuantization.Binary; break; - case 3 when qHash == FastHash._3.f32 && value.IsEqual(FastHash._3.f32_u8): + case FH.f32.Length when FH.f32.Is(qHash, value): quantType = VectorSetQuantization.None; break; - case 4 when qHash == FastHash._4.int8 && value.IsEqual(FastHash._4.int8_u8): + case FH.int8.Length when FH.int8.Is(qHash, value): quantType = VectorSetQuantization.Int8; break; default: @@ -96,16 +102,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes quantType = VectorSetQuantization.Unknown; break; } + break; - case 17 when keyHash == FastHash._17.hnsw_max_node_uid && key.IsEqual(FastHash._17.hnsw_max_node_uid_u8) && value.TryGetInt64(out var i64): + case FH.hnsw_max_node_uid.Length + when FH.hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): hnswMaxNodeUid = i64; break; } } - SetResult(message, new VectorSetInfo(quantType, quantTypeRaw, vectorDim, size, maxLevel, vsetUid, hnswMaxNodeUid)); + SetResult( + message, + new VectorSetInfo(quantType, quantTypeRaw, vectorDim, size, maxLevel, vsetUid, hnswMaxNodeUid)); return true; } + return false; } } diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 44efe09be..b13a12423 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -47,4 +47,8 @@ + + + + \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs deleted file mode 100644 index f08b9d1b3..000000000 --- a/tests/StackExchange.Redis.Tests/FastHashTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Reflection; -using System.Reflection.Emit; -using System.Text; -using Xunit; - -namespace StackExchange.Redis.Tests; - -public class FastHashTests(ITestOutputHelper log) -{ - [Fact] - public void CaseInsensitiveHash() - { - Assert.NotEqual("abc"u8.Hash64(), "ABC"u8.Hash64()); - Assert.Equal("abc"u8.Hash64CI(), "ABC"u8.Hash64CI()); - } - - [Fact] - public void CaseSensitiveEquals() - { - Assert.True("ABC"u8.SequenceEqual("ABC"u8), "same"); - Assert.False("abc"u8.SequenceEqual("ABC"u8), "off-by-case"); - } - - [Theory] - [MemberData(nameof(HashLiterals))] - public void Hash64(string type, string fieldName, long declaredHash, byte[] expectedBytes, byte[]? actualBytes, int? length) - { - Assert.NotNull(actualBytes); // missing the _u8 field, which must exist for corresponding equality test - Assert.Equal(expectedBytes, actualBytes); - var actualHash = FastHash.Hash64(actualBytes); - log.WriteLine($"{nameof(FastHash.Hash64)}: {type} {fieldName}: {actualHash}"); - Assert.Equal(declaredHash, actualHash); - - if (length.HasValue) - { - Assert.Equal(length.Value, actualBytes.Length); - } - - // check equality between hash implementations -#pragma warning disable CS0618 // Type or member is obsolete - var tmp = FastHash.Hash64Unsafe(actualBytes); - log.WriteLine($"{nameof(FastHash.Hash64Unsafe)}: {tmp}"); - Assert.Equal(actualHash, tmp); - - tmp = FastHash.Hash64Fallback(actualBytes); - log.WriteLine($"{nameof(FastHash.Hash64Fallback)}: {tmp}"); - Assert.Equal(actualHash, tmp); -#pragma warning restore CS0618 // Type or member is obsolete - } - - public static IEnumerable HashLiterals() - { - var pending = new Queue(); - pending.Enqueue(typeof(FastHash)); - while (pending.TryDequeue(out var type)) - { - // dive into nested types - foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) - { - pending.Enqueue(nested); - } - - // enforce the length if the type is named "_{N}" - object? length = null; - if (type.Name.StartsWith("_") && int.TryParse(type.Name.Substring(1), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int i)) - { - length = i; - } - - // check for relevant fields - foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) - { - if (field.Name == "CaseMask" && type == typeof(FastHash)) continue; // not a hash - - if (field.IsLiteral && field.FieldType == typeof(long)) - { - // the expected bytes match fieldName (using - for _) unless there's a [Literal] on that field - var expectedBytes = Encoding.UTF8.GetBytes( - field.GetCustomAttribute()?.Token - ?? field.Name.Replace("_", "-")); - var declaredHash = (long)field.GetRawConstantValue()!; - - byte[]? actualBytes = null; - var u8Prop = type.GetProperty(field.Name + "_u8"); - if (u8Prop != null && u8Prop.PropertyType == typeof(ReadOnlySpan)) - { - actualBytes = ReadBytes(u8Prop); - } - yield return [type.Name, field.Name, declaredHash, expectedBytes, actualBytes, length]; - } - } - } - } - - private static readonly MethodInfo ReadOnlySpanToArray = typeof(ReadOnlySpan).GetMethod(nameof(ReadOnlySpan.ToArray))!; - private static byte[]? ReadBytes(PropertyInfo prop) - { - var getter = prop.GetMethod; - if (getter is not { IsStatic: true } || getter.ReturnType != typeof(ReadOnlySpan)) return null; - - // we can't use prop.GetValue() because it's a ref struct (cannot be boxed); instead, we need to use ref-emit - DynamicMethod dm = new DynamicMethod(prop.Name, typeof(byte[]), null, typeof(FastHashTests).Module); - ILGenerator il = dm.GetILGenerator(); - var loc = il.DeclareLocal(typeof(ReadOnlySpan)); - il.EmitCall(OpCodes.Call, getter, null); - il.Emit(OpCodes.Stloc, loc); - il.Emit(OpCodes.Ldloca, loc); - il.EmitCall(OpCodes.Call, ReadOnlySpanToArray, null); - il.Emit(OpCodes.Ret); - return (byte[]?)dm.Invoke(null, null); - } -} From b14934dbe89f064735610d92a285ca415214c600 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 16:57:22 +0100 Subject: [PATCH 20/32] Move the literals to be better scoped. --- .../FastHashGenerator.cs | 50 +++++++++++++++---- .../FastHashGenerator.md | 13 +++-- src/StackExchange.Redis/FastHash.Literals.cs | 20 -------- src/StackExchange.Redis/FastHash.cs | 33 ++++-------- .../ResultProcessor.VectorSets.cs | 45 +++++++++++------ 5 files changed, 89 insertions(+), 72 deletions(-) delete mode 100644 src/StackExchange.Redis/FastHash.Literals.cs diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs index a5add2af5..bd8807da4 100644 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.cs +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.cs @@ -40,6 +40,24 @@ private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) return false; } + private static string GetName(INamedTypeSymbol type) + { + if (type.ContainingType is null) return type.Name; + var stack = new Stack(); + while (true) + { + stack.Push(type.Name); + if (type.ContainingType is null) break; + type = type.ContainingType; + } + var sb = new StringBuilder(stack.Pop()); + while (stack.Count != 0) + { + sb.Append('.').Append(stack.Pop()); + } + return sb.ToString(); + } + private (string Namespace, string ParentType, string Name, string Value) Transform( GeneratorSyntaxContext ctx, CancellationToken cancellationToken) @@ -49,7 +67,7 @@ private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) string ns = "", parentType = ""; if (named.ContainingType is { } containingType) { - parentType = containingType.Name; // don't worry about multi-level nesting for now; add later if needed + parentType = GetName(containingType); ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); } else if (named.ContainingNamespace is { } containingNamespace) @@ -114,17 +132,33 @@ private void Generate( foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType))) { NewLine(); + int braces = 0; if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) { NewLine().Append("namespace ").Append(grp.Key.Namespace); NewLine().Append("{"); indent++; + braces++; } if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) { - NewLine().Append("partial class ").Append(grp.Key.ParentType); - NewLine().Append("{"); - indent++; + if (grp.Key.ParentType.Contains('.')) // nested types + { + foreach (var part in grp.Key.ParentType.Split('.')) + { + NewLine().Append("partial class ").Append(part); + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + braces++; + } } foreach (var literal in grp) @@ -156,12 +190,8 @@ private void Generate( NewLine().Append("}"); } - if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) - { - indent--; - NewLine().Append("}"); - } - if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + // handle any closing braces + while (braces-- > 0) { indent--; NewLine().Append("}"); diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md index 2a7408ed8..c45a1dc04 100644 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.md +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.md @@ -11,7 +11,8 @@ The purpose of this generator is to interpret inputs like: Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier. Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`. -The generator demands *all* of `[FastHash] public static partial class`. +The generator demands *all* of `[FastHash] public static partial class`, and note that any *containing* types must +*also* be declared `partial`. The output is of the form: @@ -34,7 +35,9 @@ static partial class f32 } ``` -This allows for fast, efficient, and safe matching of well-known tokens, for example: +(this API is strictly an internal implementation detail, and can change at any time) + +This generated code allows for fast, efficient, and safe matching of well-known tokens, for example: ``` c# var key = ... @@ -52,7 +55,11 @@ switch (key.Length) The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches -must also perform a sequence equality check - the `Is` convenient method validates both hash and equality. +must also perform a sequence equality check - the `Is` convenient method validates both hash and equality. + +Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties +that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but +easy to return via a property.~~~~ diff --git a/src/StackExchange.Redis/FastHash.Literals.cs b/src/StackExchange.Redis/FastHash.Literals.cs deleted file mode 100644 index 44cb281ba..000000000 --- a/src/StackExchange.Redis/FastHash.Literals.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace StackExchange.Redis; - -[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "To better represent the expected literals")] -internal static partial class FastHash -{ - // see HastHashGenerator.md for more information and intended usage. -#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 - [FastHash] public static partial class bin { } - [FastHash] public static partial class f32 { } - [FastHash] public static partial class int8 { } - [FastHash] public static partial class size { } - [FastHash] public static partial class vset_uid { } - [FastHash] public static partial class max_level { } - [FastHash] public static partial class quant_type { } - [FastHash] public static partial class vector_dim { } - [FastHash] public static partial class hnsw_max_node_uid { } -#pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 -} diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs index f8b9e5ad4..61d80c485 100644 --- a/src/StackExchange.Redis/FastHash.cs +++ b/src/StackExchange.Redis/FastHash.cs @@ -1,6 +1,6 @@ -using System; -using System.Buffers; +using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -11,29 +11,16 @@ namespace StackExchange.Redis; /// RESP literals that are usually identifiable by their length and initial bytes; it is not intended /// for general purpose hashing. All matches must also perform a sequence equality check. /// -/// While introduced alongside the VSET work, this is not specific to VSET - and indeed VSET -/// is not a good example of the feature; rather, this is intended for more widespread use. -/* -Example data from the benchmarks; note that string is included only for baseline purposes - we don't actually want -to construct strings when parsing tokens. - -| Method | Size | Mean | Error | StdDev | Median | Op/s | Ratio | RatioSD | Allocated | Alloc Ratio | -|--------------------- |----- |----------:|----------:|----------:|----------:|--------------:|------:|--------:|----------:|------------:| -| String | 16 | 21.376 ns | 0.4164 ns | 0.6483 ns | 21.268 ns | 46,781,518.5 | 1.00 | 0.04 | - | NA | -| Hash64 | 16 | 3.161 ns | 0.0605 ns | 0.0647 ns | 3.148 ns | 316,400,326.5 | 0.15 | 0.01 | - | NA | -| Hash64Unsafe | 16 | 3.820 ns | 0.0747 ns | 0.1072 ns | 3.811 ns | 261,789,013.8 | 0.18 | 0.01 | - | NA | -| Hash64Fallback | 16 | 19.461 ns | 0.2954 ns | 0.2763 ns | 19.496 ns | 51,383,837.0 | 0.91 | 0.03 | - | NA | -| Hash64_SingleSegment | 16 | 9.477 ns | 0.1877 ns | 0.3705 ns | 9.464 ns | 105,519,833.1 | 0.44 | 0.02 | - | NA | -| Hash64_MultiSegment | 16 | 82.778 ns | 1.6255 ns | 2.3313 ns | 82.475 ns | 12,080,568.6 | 3.88 | 0.16 | - | NA | -*/ -internal static partial class FastHash +/// See HastHashGenerator.md for more information and intended usage. +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +[Conditional("DEBUG")] // evaporate in release +internal sealed class FastHashAttribute(string token = "") : Attribute { - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class FastHashAttribute(string token = "") : Attribute - { - public string Token => token; - } + public string Token => token; +} +internal static class FastHash +{ // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves // our entropy, but is still useful when case doesn't matter. private const long CaseMask = ~0x2020202020202020; diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index 2383f33d1..f52170189 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -1,5 +1,4 @@ using Pipelines.Sockets.Unofficial.Arenas; -using FH = global::StackExchange.Redis.FastHash; // ReSharper disable once CheckNamespace namespace StackExchange.Redis; @@ -44,7 +43,7 @@ protected override bool TryReadOne(in RawResult result, out RedisValue value) } } - private sealed class VectorSetInfoProcessor : ResultProcessor + private sealed partial class VectorSetInfoProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -59,7 +58,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var quantType = VectorSetQuantization.Unknown; string? quantTypeRaw = null; int vectorDim = 0, maxLevel = 0; - long size = 0, vsetUid = 0, hnswMaxNodeUid = 0; + long resultSize = 0, vsetUid = 0, hnswMaxNodeUid = 0; var iter = result.GetItems().GetEnumerator(); while (iter.MoveNext()) { @@ -71,30 +70,30 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var keyHash = key.Payload.Hash64(); switch (key.Payload.Length) { - case FH.size.Length when FH.size.Is(keyHash, key) && value.TryGetInt64(out var i64): - size = i64; + case size.Length when size.Is(keyHash, key) && value.TryGetInt64(out var i64): + resultSize = i64; break; - case FH.vset_uid.Length when FH.vset_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): + case vset_uid.Length when vset_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): vsetUid = i64; break; - case FH.max_level.Length when FH.max_level.Is(keyHash, key) && value.TryGetInt64(out var i64): + case max_level.Length when max_level.Is(keyHash, key) && value.TryGetInt64(out var i64): maxLevel = checked((int)i64); break; - case FH.vector_dim.Length - when FH.vector_dim.Is(keyHash, key) && value.TryGetInt64(out var i64): + case vector_dim.Length + when vector_dim.Is(keyHash, key) && value.TryGetInt64(out var i64): vectorDim = checked((int)i64); break; - case FH.quant_type.Length when FH.quant_type.Is(keyHash, key): + case quant_type.Length when quant_type.Is(keyHash, key): var qHash = value.Payload.Hash64(); switch (value.Payload.Length) { - case FH.bin.Length when FH.bin.Is(qHash, value): + case bin.Length when bin.Is(qHash, value): quantType = VectorSetQuantization.Binary; break; - case FH.f32.Length when FH.f32.Is(qHash, value): + case f32.Length when f32.Is(qHash, value): quantType = VectorSetQuantization.None; break; - case FH.int8.Length when FH.int8.Is(qHash, value): + case int8.Length when int8.Is(qHash, value): quantType = VectorSetQuantization.Int8; break; default: @@ -104,8 +103,8 @@ when FH.vector_dim.Is(keyHash, key) && value.TryGetInt64(out var i64): } break; - case FH.hnsw_max_node_uid.Length - when FH.hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): + case hnsw_max_node_uid.Length + when hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): hnswMaxNodeUid = i64; break; } @@ -113,11 +112,25 @@ when FH.hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): SetResult( message, - new VectorSetInfo(quantType, quantTypeRaw, vectorDim, size, maxLevel, vsetUid, hnswMaxNodeUid)); + new VectorSetInfo(quantType, quantTypeRaw, vectorDim, resultSize, maxLevel, vsetUid, hnswMaxNodeUid)); return true; } return false; } + +#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 + // ReSharper disable InconsistentNaming - to better represent expected literals + [FastHash] public static partial class bin { } + [FastHash] public static partial class f32 { } + [FastHash] public static partial class int8 { } + [FastHash] public static partial class size { } + [FastHash] public static partial class vset_uid { } + [FastHash] public static partial class max_level { } + [FastHash] public static partial class quant_type { } + [FastHash] public static partial class vector_dim { } + [FastHash] public static partial class hnsw_max_node_uid { } + // ReSharper restore InconsistentNaming +#pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 } } From 26a4a6285873b2f2e4c8c029757ead28480cb765 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 17:03:56 +0100 Subject: [PATCH 21/32] tyop --- eng/StackExchange.Redis.Build/FastHashGenerator.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md index c45a1dc04..847add6b3 100644 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.md +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.md @@ -55,11 +55,8 @@ switch (key.Length) The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches -must also perform a sequence equality check - the `Is` convenient method validates both hash and equality. +must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality. Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but -easy to return via a property.~~~~ - - - +easy to return via a property. From f9d3af92a4b1a00dc78d9f1c59fe35f9facdc157 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 17:13:03 +0100 Subject: [PATCH 22/32] lost a using directive somehow --- src/StackExchange.Redis/FastHash.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs index 61d80c485..03f620b86 100644 --- a/src/StackExchange.Redis/FastHash.cs +++ b/src/StackExchange.Redis/FastHash.cs @@ -1,4 +1,5 @@ -using System.Buffers; +using System; +using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; using System.Runtime.CompilerServices; From 51f93c66ee684ccc49a70fe08a3b5f3fddfb8d11 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 17:16:47 +0100 Subject: [PATCH 23/32] disable spell-checker on literals --- src/StackExchange.Redis/ResultProcessor.VectorSets.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index f52170189..6fed7c2fc 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -121,6 +121,7 @@ when hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): #pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // ReSharper disable InconsistentNaming - to better represent expected literals + // ReSharper disable IdentifierTypo [FastHash] public static partial class bin { } [FastHash] public static partial class f32 { } [FastHash] public static partial class int8 { } @@ -131,6 +132,7 @@ [FastHash] public static partial class quant_type { } [FastHash] public static partial class vector_dim { } [FastHash] public static partial class hnsw_max_node_uid { } // ReSharper restore InconsistentNaming + // ReSharper restore IdentifierTypo #pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 } } From c421ccbd93268945ad04a0a4914f68e06db2a568 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 14 Aug 2025 17:17:39 +0100 Subject: [PATCH 24/32] literals can be private --- .../ResultProcessor.VectorSets.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index 6fed7c2fc..8743ebd0b 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -122,15 +122,15 @@ when hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): #pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // ReSharper disable InconsistentNaming - to better represent expected literals // ReSharper disable IdentifierTypo - [FastHash] public static partial class bin { } - [FastHash] public static partial class f32 { } - [FastHash] public static partial class int8 { } - [FastHash] public static partial class size { } - [FastHash] public static partial class vset_uid { } - [FastHash] public static partial class max_level { } - [FastHash] public static partial class quant_type { } - [FastHash] public static partial class vector_dim { } - [FastHash] public static partial class hnsw_max_node_uid { } + [FastHash] private static partial class bin { } + [FastHash] private static partial class f32 { } + [FastHash] private static partial class int8 { } + [FastHash] private static partial class size { } + [FastHash] private static partial class vset_uid { } + [FastHash] private static partial class max_level { } + [FastHash] private static partial class quant_type { } + [FastHash] private static partial class vector_dim { } + [FastHash] private static partial class hnsw_max_node_uid { } // ReSharper restore InconsistentNaming // ReSharper restore IdentifierTypo #pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 From 536efe416db4825eeba3c0207fc9b457353ce3c2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 15 Aug 2025 11:29:40 +0100 Subject: [PATCH 25/32] - add FastHashTests - misc FastHash core improvements --- .../FastHashGenerator.cs | 21 +++- .../FastHashGenerator.md | 8 +- src/StackExchange.Redis/FastHash.cs | 8 +- .../FastHashTests.cs | 112 ++++++++++++++++++ .../StackExchange.Redis.Tests.csproj | 1 + 5 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/FastHashTests.cs diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs index bd8807da4..cdbc94ebe 100644 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.cs +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.cs @@ -175,17 +175,28 @@ private void Generate( } } + // perform string escaping on the generated value (this includes the quotes, note) + var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString(); + var hash = FastHash.Hash64(buffer.AsSpan(0, len)); NewLine().Append("static partial class ").Append(literal.Name); NewLine().Append("{"); indent++; NewLine().Append("public const int Length = ").Append(len).Append(';'); NewLine().Append("public const long Hash = ").Append(hash).Append(';'); - NewLine().Append("public static ReadOnlySpan U8 => @\"") - .Append(literal.Value.Replace("\"", "\"\"")).Append("\"u8;"); - NewLine().Append("public static string Text => @\"") - .Append(literal.Value.Replace("\"", "\"\"")).Append("\";"); - NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);"); + NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;"); + NewLine().Append("public const string Text = ").Append(csValue).Append(';'); + if (len <= 8) + { + // the hash enforces all the values + NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;"); + NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash & value.Length == Length;"); + } + else + { + NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);"); + NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash && value.SequenceEqual(U8);"); + } indent--; NewLine().Append("}"); } diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md index 847add6b3..7fc5103ae 100644 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.md +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.md @@ -23,15 +23,17 @@ static partial class bin public const long Hash = 7235938; public static ReadOnlySpan U8 => @"bin"u8; public static string Text => @"bin"; - public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8); + public static bool Is(long hash, in RawResult value) => ... + public static bool Is(long hash, in ReadOnlySpan value) => ... } static partial class f32 { public const int Length = 3; public const long Hash = 3289958; public static ReadOnlySpan U8 => @"f32"u8; - public static string Text => @"f32"; - public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8); + public const string Text = @"f32"; + public static bool Is(long hash, in RawResult value) => ... + public static bool Is(long hash, in ReadOnlySpan value) => ... } ``` diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs index 03f620b86..49eb01b31 100644 --- a/src/StackExchange.Redis/FastHash.cs +++ b/src/StackExchange.Redis/FastHash.cs @@ -22,12 +22,17 @@ internal sealed class FastHashAttribute(string token = "") : Attribute internal static class FastHash { + /* not sure we need this, but: retain for reference + // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves // our entropy, but is still useful when case doesn't matter. private const long CaseMask = ~0x2020202020202020; public static long Hash64CI(this ReadOnlySequence value) => value.Hash64() & CaseMask; + public static long Hash64CI(this scoped ReadOnlySpan value) + => value.Hash64() & CaseMask; +*/ public static long Hash64(this ReadOnlySequence value) { @@ -57,9 +62,6 @@ static long SlowHash64(ReadOnlySequence value) } } - public static long Hash64CI(this scoped ReadOnlySpan value) - => value.Hash64() & CaseMask; - public static long Hash64(this scoped ReadOnlySpan value) { if (BitConverter.IsLittleEndian) diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs new file mode 100644 index 000000000..418198cfd --- /dev/null +++ b/tests/StackExchange.Redis.Tests/FastHashTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // names are weird in this test! +// ReSharper disable InconsistentNaming - to better represent expected literals +// ReSharper disable IdentifierTypo +namespace StackExchange.Redis.Tests; + +public partial class FastHashTests +{ + // note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter + // what it *is* - what matters is that we can see that it has entropy between different values + [Theory] + [InlineData(1, a.Length, a.Text, a.Hash, 97)] + [InlineData(2, ab.Length, ab.Text, ab.Hash, 25185)] + [InlineData(3, abc.Length, abc.Text, abc.Hash, 6513249)] + [InlineData(4, abcd.Length, abcd.Text, abcd.Hash, 1684234849)] + [InlineData(5, abcde.Length, abcde.Text, abcde.Hash, 435475931745)] + [InlineData(6, abcdef.Length, abcdef.Text, abcdef.Hash, 112585661964897)] + [InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.Hash, 29104508263162465)] + [InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.Hash, 7523094288207667809)] + + [InlineData(1, x.Length, x.Text, x.Hash, 120)] + [InlineData(2, xx.Length, xx.Text, xx.Hash, 30840)] + [InlineData(3, xxx.Length, xxx.Text, xxx.Hash, 7895160)] + [InlineData(4, xxxx.Length, xxxx.Text, xxxx.Hash, 2021161080)] + [InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.Hash, 517417236600)] + [InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.Hash, 132458812569720)] + [InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.Hash, 33909456017848440)] + [InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.Hash, 8680820740569200760)] + + [InlineData(3, 窓.Length, 窓.Text, 窓.Hash, 9677543, "窓")] + [InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.Hash, 7523094288207667809)] + + // show that foo_bar is interpreted as foo-bar + [InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.Hash, 32195221641981798, "foo-bar", nameof(foo_bar))] + [InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.Hash, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))] + [InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.Hash, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))] + public void Validate(int expectedLength, int actualLength, string actualValue, long actualHash, long expectedHash, string? expectedValue = null, string originForDisambiguation = "") + { + _ = originForDisambiguation; // to allow otherwise-identical test data to coexist + Assert.Equal(expectedLength, actualLength); + Assert.Equal(expectedHash, actualHash); + var bytes = Encoding.UTF8.GetBytes(actualValue); + Assert.Equal(expectedLength, bytes.Length); + Assert.Equal(expectedHash, FastHash.Hash64(bytes)); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Equal(expectedHash, FastHash.Hash64Fallback(bytes)); +#pragma warning restore CS0618 // Type or member is obsolete + if (expectedValue is not null) + { + Assert.Equal(expectedValue, actualValue); + } + } + + [Fact] + public void FastHashIs_Short() + { + ReadOnlySpan value = "abc"u8; + var hash = value.Hash64(); + Assert.Equal(abc.Hash, hash); + Assert.True(abc.Is(hash, value)); + + value = "abz"u8; + hash = value.Hash64(); + Assert.NotEqual(abc.Hash, hash); + Assert.False(abc.Is(hash, value)); + } + + [Fact] + public void FastHashIs_Long() + { + ReadOnlySpan value = "abcdefghijklmnopqrst"u8; + var hash = value.Hash64(); + Assert.Equal(abcdefghijklmnopqrst.Hash, hash); + Assert.True(abcdefghijklmnopqrst.Is(hash, value)); + + value = "abcdefghijklmnopqrsz"u8; + hash = value.Hash64(); + Assert.Equal(abcdefghijklmnopqrst.Hash, hash); // hash collision, fine + Assert.False(abcdefghijklmnopqrst.Is(hash, value)); + } + + [FastHash] private static partial class a { } + [FastHash] private static partial class ab { } + [FastHash] private static partial class abc { } + [FastHash] private static partial class abcd { } + [FastHash] private static partial class abcde { } + [FastHash] private static partial class abcdef { } + [FastHash] private static partial class abcdefg { } + [FastHash] private static partial class abcdefgh { } + + [FastHash] private static partial class abcdefghijklmnopqrst { } + + // show that foo_bar and foo-bar are different + [FastHash] private static partial class foo_bar { } + [FastHash("foo-bar")] private static partial class foo_bar_hyphen { } + [FastHash("foo_bar")] private static partial class foo_bar_underscore { } + + [FastHash] private static partial class 窓 { } + + [FastHash] private static partial class x { } + [FastHash] private static partial class xx { } + [FastHash] private static partial class xxx { } + [FastHash] private static partial class xxxx { } + [FastHash] private static partial class xxxxx { } + [FastHash] private static partial class xxxxxx { } + [FastHash] private static partial class xxxxxxx { } + [FastHash] private static partial class xxxxxxxx { } +} diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 50d4ae3d1..f6e38236b 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -30,5 +30,6 @@ + From d53f1ab75caad1cd89f10598c3fb850316a01597 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 4 Sep 2025 17:01:47 +0100 Subject: [PATCH 26/32] fix PR nits --- src/Directory.Build.props | 3 + .../Interfaces/IDatabase.VectorSets.cs | 56 +----- .../Interfaces/IDatabaseAsync.VectorSets.cs | 31 +-- .../KeyPrefixed.VectorSets.cs | 30 +-- .../KeyPrefixedDatabase.VectorSets.cs | 30 +-- .../PublicAPI/PublicAPI.Unshipped.txt | 30 ++- .../RedisDatabase.VectorSets.cs | 124 +----------- .../VectorSetSimilaritySearchMessage.cs | 128 ++---------- .../VectorSetSimilaritySearchRequest.cs | 190 ++++++++++++++++++ .../KeyPrefixedVectorSetTests.cs | 61 +++--- .../VectorSetIntegrationTests.cs | 130 ++++++++---- 11 files changed, 381 insertions(+), 432 deletions(-) create mode 100644 src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 29eadff61..06e403ebb 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,4 +10,7 @@ + + + diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs index 3872ba176..741f968ab 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -182,63 +182,13 @@ bool VectorSetSetAttributesJson( /// Find similar vectors using vector similarity search. /// /// The key of the vectorset. - /// The query vector. - /// The number of similar vectors to return (COUNT parameter). - /// Whether to include similarity scores in the results (WITHSCORES parameter). - /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). - /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). - /// Optional search exploration factor for better recall (EF parameter). - /// Optional filter expression to restrict results (FILTER parameter); . - /// Optional maximum filtering attempts (FILTER-EF parameter). - /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). - /// Whether to run search in main thread (NOTHREAD parameter). + /// The query to execute. /// The flags to use for this operation. /// Similar vectors with their similarity scores. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetSimilaritySearchByVector( + Lease? VectorSetSimilaritySearch( RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None); - - /// - /// Find similar vectors to an existing member. - /// - /// The key of the vectorset. - /// The member to find similar vectors for. - /// The number of similar vectors to return (COUNT parameter). - /// Whether to include similarity scores in the results (WITHSCORES parameter). - /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). - /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). - /// Optional search exploration factor for better recall (EF parameter). - /// Optional filter expression to restrict results (FILTER parameter). - /// Optional maximum filtering attempts (FILTER-EF parameter). - /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). - /// Whether to run search in main thread (NOTHREAD parameter). - /// The flags to use for this operation. - /// Similar vectors with their similarity scores. - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Lease? VectorSetSimilaritySearchByMember( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, + VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None); } diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs index 8f4a6d511..de60b2a3b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -90,35 +90,10 @@ Task VectorSetSetAttributesJsonAsync( string jsonAttributes, CommandFlags flags = CommandFlags.None); - /// + /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetSimilaritySearchByVectorAsync( + Task?> VectorSetSimilaritySearchAsync( RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None); - - /// - [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] - Task?> VectorSetSimilaritySearchByMemberAsync( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, + VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None); } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs index d6e7af940..403a38396 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -58,33 +58,9 @@ public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandF public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, jsonAttributes, flags); - public Task?> VectorSetSimilaritySearchByVectorAsync( + public Task?> VectorSetSimilaritySearchAsync( RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, + VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByVectorAsync(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - - public Task?> VectorSetSimilaritySearchByMemberAsync( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByMemberAsync(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + Inner.VectorSetSimilaritySearchAsync(ToInner(key), query, flags); } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs index 21149c866..9c24961d1 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs @@ -55,33 +55,9 @@ public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => Inner.VectorSetSetAttributesJson(ToInner(key), member, jsonAttributes, flags); - public Lease? VectorSetSimilaritySearchByVector( + public Lease? VectorSetSimilaritySearch( RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, + VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByVector(ToInner(key), vector, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); - - public Lease? VectorSetSimilaritySearchByMember( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSimilaritySearchByMember(ToInner(key), member, count, withScores, withAttributes, epsilon, searchExplorationFactor, filterExpression, maxFilteringEffort, useExactSearch, disableThreading, flags); + Inner.VectorSetSimilaritySearch(ToInner(key), query, flags); } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 4b349c738..3ac3b0455 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,28 @@ #nullable enable +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.get -> double? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.get -> string? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Member.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Vector.get -> System.ReadOnlyMemory +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Vector.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.VectorSetSimilaritySearchRequest() -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.set -> void [SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! [SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! [SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -14,8 +38,7 @@ [SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! [SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearchByVector(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -29,8 +52,7 @@ [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchByVectorAsync(StackExchange.Redis.RedisKey key, System.ReadOnlyMemory vector, int? count = null, bool withScores = false, bool withAttributes = false, double? epsilon = null, int? searchExplorationFactor = null, string? filterExpression = null, int? maxFilteringEffort = null, bool useExactSearch = false, bool disableThreading = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! [SER001]StackExchange.Redis.VectorSetInfo [SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int [SER001]StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs index cc5a8bd65..04ec9f2c7 100644 --- a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -94,67 +94,13 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j return ExecuteSync(msg, ResultProcessor.Boolean); } - public Lease? VectorSetSimilaritySearchByVector( + public Lease? VectorSetSimilaritySearch( RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, + VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None) { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - RedisValue.Null, - vector, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); - return ExecuteSync(msg, msg.GetResultProcessor()); - } - - public Lease? VectorSetSimilaritySearchByMember( - RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - member, - ReadOnlyMemory.Empty, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); + if (query == null) throw new ArgumentNullException(nameof(query)); + var msg = query.ToMessage(key, Database, flags); return ExecuteSync(msg, msg.GetResultProcessor()); } @@ -247,67 +193,13 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe return ExecuteAsync(msg, ResultProcessor.Boolean); } - public Task?> VectorSetSimilaritySearchByVectorAsync( - RedisKey key, - ReadOnlyMemory vector, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, - CommandFlags flags = CommandFlags.None) - { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - RedisValue.Null, - vector, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); - return ExecuteAsync(msg, msg.GetResultProcessor()); - } - - public Task?> VectorSetSimilaritySearchByMemberAsync( + public Task?> VectorSetSimilaritySearchAsync( RedisKey key, - RedisValue member, - int? count = null, - bool withScores = false, - bool withAttributes = false, - double? epsilon = null, - int? searchExplorationFactor = null, - string? filterExpression = null, - int? maxFilteringEffort = null, - bool useExactSearch = false, - bool disableThreading = false, + VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None) { - var msg = new VectorSetSimilaritySearchMessage( - Database, - flags, - key, - member, - ReadOnlyMemory.Empty, - count, - withScores, - withAttributes, - epsilon, - searchExplorationFactor, - filterExpression, - maxFilteringEffort, - useExactSearch, - disableThreading); + if (query == null) throw new ArgumentNullException(nameof(query)); + var msg = query.ToMessage(key, Database, flags); return ExecuteAsync(msg, msg.GetResultProcessor()); } } diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs index 9fd6a5a13..45e0a27d8 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs @@ -5,34 +5,16 @@ namespace StackExchange.Redis; internal sealed class VectorSetSimilaritySearchMessage( int db, CommandFlags flags, + VectorSetSimilaritySearchMessage.VsimFlags vsimFlags, RedisKey key, RedisValue member, ReadOnlyMemory vector, - int? count, - bool withScores, - bool withAttributes, - double? epsilon, - int? searchExplorationFactor, + int count, + double epsilon, + int searchExplorationFactor, string? filterExpression, - int? maxFilteringEffort, - bool useExactSearch, - bool disableThreading) : Message(db, flags, RedisCommand.VSIM) + int maxFilteringEffort) : Message(db, flags, RedisCommand.VSIM) { - private readonly VsimFlags _flags = - (count.HasValue ? VsimFlags.Count : 0) | - (withScores ? VsimFlags.WithScores : 0) | - (withAttributes ? VsimFlags.WithAttributes : 0) | - (useExactSearch ? VsimFlags.UseExactSearch : 0) | - (disableThreading ? VsimFlags.DisableThreading : 0) | - (epsilon.HasValue ? VsimFlags.Epsilon : 0) | - (searchExplorationFactor.HasValue ? VsimFlags.SearchExplorationFactor : 0) | - (maxFilteringEffort.HasValue ? VsimFlags.MaxFilteringEffort : 0); - - private readonly double _epsilon = epsilon.GetValueOrDefault(); - private readonly int _count = count.GetValueOrDefault(); - private readonly int _searchExplorationFactor = searchExplorationFactor.GetValueOrDefault(); - private readonly int _maxFilteringEffort = maxFilteringEffort.GetValueOrDefault(); - public ResultProcessor?> GetResultProcessor() => VectorSetSimilaritySearchProcessor.Instance; private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> { @@ -111,7 +93,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } [Flags] - private enum VsimFlags + internal enum VsimFlags { None = 0, Count = 1 << 0, @@ -122,9 +104,10 @@ private enum VsimFlags Epsilon = 1 << 5, SearchExplorationFactor = 1 << 6, MaxFilteringEffort = 1 << 7, + FilterExpression = 1 << 8, } - private bool HasFlag(VsimFlags flag) => (_flags & flag) != 0; + private bool HasFlag(VsimFlags flag) => (vsimFlags & flag) != 0; public override int ArgCount => GetArgCount(VectorSetAddMessage.UseFp32); @@ -141,7 +124,7 @@ private int GetArgCount(bool useFp32) if (HasFlag(VsimFlags.Count)) argCount += 2; // [COUNT {count}] if (HasFlag(VsimFlags.Epsilon)) argCount += 2; // [EPSILON {epsilon}] if (HasFlag(VsimFlags.SearchExplorationFactor)) argCount += 2; // [EF {search-exploration-factor}] - if (filterExpression is not null) argCount += 2; // [FILTER {filterExpression}] + if (HasFlag(VsimFlags.FilterExpression)) argCount += 2; // [FILTER {filterExpression}] if (HasFlag(VsimFlags.MaxFilteringEffort)) argCount += 2; // [FILTER-EF {max-filtering-effort}] if (HasFlag(VsimFlags.UseExactSearch)) argCount++; // [TRUTH] if (HasFlag(VsimFlags.DisableThreading)) argCount++; // [NOTHREAD] @@ -196,22 +179,22 @@ protected override void WriteImpl(PhysicalConnection physical) if (HasFlag(VsimFlags.Count)) { physical.WriteBulkString("COUNT"u8); - physical.WriteBulkString(_count); + physical.WriteBulkString(count); } if (HasFlag(VsimFlags.Epsilon)) { physical.WriteBulkString("EPSILON"u8); - physical.WriteBulkString(_epsilon); + physical.WriteBulkString(epsilon); } if (HasFlag(VsimFlags.SearchExplorationFactor)) { physical.WriteBulkString("EF"u8); - physical.WriteBulkString(_searchExplorationFactor); + physical.WriteBulkString(searchExplorationFactor); } - if (filterExpression is not null) + if (HasFlag(VsimFlags.FilterExpression)) { physical.WriteBulkString("FILTER"u8); physical.WriteBulkString(filterExpression); @@ -220,7 +203,7 @@ protected override void WriteImpl(PhysicalConnection physical) if (HasFlag(VsimFlags.MaxFilteringEffort)) { physical.WriteBulkString("FILTER-EF"u8); - physical.WriteBulkString(_maxFilteringEffort); + physical.WriteBulkString(maxFilteringEffort); } if (HasFlag(VsimFlags.UseExactSearch)) @@ -236,87 +219,4 @@ protected override void WriteImpl(PhysicalConnection physical) public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - - /* - private int GetArgCount(bool useFp32) - { - var count = 4; // key, element and either "FP32 {vector}" or VALUES {num}" - if (reducedDimensions.HasValue) count += 2; // [REDUCE {dim}] - - if (!useFp32) count += values.Length; // {vector} in the VALUES case - - if (useCheckAndSet) count++; // [CAS] - count += quantizationType switch - { - VectorQuantizationType.None or VectorQuantizationType.Binary => 1, // [NOQUANT] or [BIN] - VectorQuantizationType.Int8 => 0, // implicit - _ => throw new ArgumentOutOfRangeException(nameof(quantizationType)), - }; - - if (buildExplorationFactor.HasValue) count += 2; // [EF {build-exploration-factor}] - if (attributesJson is not null) count += 2; // [SETATTR {attributes}] - if (maxConnections.HasValue) count += 2; // [M {numlinks}] - return count; - } - - protected override void WriteImpl(PhysicalConnection physical) - { - bool useFp32 = UseFp32; // snapshot to avoid race in debug scenarios - physical.WriteHeader(Command, GetArgCount(useFp32)); - physical.Write(key); - if (reducedDimensions.HasValue) - { - physical.WriteBulkString("REDUCE"u8); - physical.WriteBulkString(reducedDimensions.GetValueOrDefault()); - } - if (useFp32) - { - physical.WriteBulkString("FP32"u8); - physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span)); - } - else - { - physical.WriteBulkString("VALUES"u8); - physical.WriteBulkString(values.Length); - foreach (var val in values.Span) - { - physical.WriteBulkString(val); - } - } - physical.WriteBulkString(element); - if (useCheckAndSet) physical.WriteBulkString("CAS"u8); - - switch (quantizationType) - { - case VectorQuantizationType.Int8: - break; - case VectorQuantizationType.None: - physical.WriteBulkString("NOQUANT"u8); - break; - case VectorQuantizationType.Binary: - physical.WriteBulkString("BIN"u8); - break; - default: - throw new ArgumentOutOfRangeException(nameof(quantizationType)); - } - if (buildExplorationFactor.HasValue) - { - physical.WriteBulkString("EF"u8); - physical.WriteBulkString(buildExplorationFactor.GetValueOrDefault()); - } - if (attributesJson is not null) - { - physical.WriteBulkString("SETATTR"u8); - physical.WriteBulkString(attributesJson); - } - if (maxConnections.HasValue) - { - physical.WriteBulkString("M"u8); - physical.WriteBulkString(maxConnections.GetValueOrDefault()); - } - } - - public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - => serverSelectionStrategy.HashSlot(key); - */ } diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs new file mode 100644 index 000000000..9480a8120 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs @@ -0,0 +1,190 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using VsimFlags = StackExchange.Redis.VectorSetSimilaritySearchMessage.VsimFlags; + +namespace StackExchange.Redis; + +/// +/// Represents the request for a vector similarity search operation. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public class VectorSetSimilaritySearchRequest +{ + /// + /// The query vector. + /// + public ReadOnlyMemory Vector { get; set; } + + /// + /// The member to find similar vectors for. + /// + public RedisValue Member { get; set; } + + private VsimFlags _vsimFlags; + + // use the flags to reduce storage from N*Nullable + private int _searchExplorationFactor, _maxFilteringEffort, _count; + private double _epsilon; + + private bool HasFlag(VsimFlags flag) => (_vsimFlags & flag) != 0; + + private void SetFlag(VsimFlags flag, bool value) + { + if (value) + { + _vsimFlags |= flag; + } + else + { + _vsimFlags &= ~flag; + } + } + + /// + /// The number of similar vectors to return (COUNT parameter). + /// + public int? Count + { + get => HasFlag(VsimFlags.Count) ? _count : null; + set + { + if (value.HasValue) + { + _count = value.GetValueOrDefault(); + SetFlag(VsimFlags.Count, true); + } + else + { + SetFlag(VsimFlags.Count, false); + } + } + } + + /// + /// Whether to include similarity scores in the results (WITHSCORES parameter). + /// + public bool WithScores + { + get => HasFlag(VsimFlags.WithScores); + set => SetFlag(VsimFlags.WithScores, value); + } + + /// + /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). + /// + public bool WithAttributes + { + get => HasFlag(VsimFlags.WithAttributes); + set => SetFlag(VsimFlags.WithAttributes, value); + } + + /// + /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). + /// + public double? Epsilon + { + get => HasFlag(VsimFlags.Epsilon) ? _epsilon : null; + set + { + if (value.HasValue) + { + _epsilon = value.GetValueOrDefault(); + SetFlag(VsimFlags.Epsilon, true); + } + else + { + SetFlag(VsimFlags.Epsilon, false); + } + } + } + + /// + /// Optional search exploration factor for better recall (EF parameter). + /// + public int? SearchExplorationFactor + { + get => HasFlag(VsimFlags.SearchExplorationFactor) ? _searchExplorationFactor : null; + set + { + if (value.HasValue) + { + _searchExplorationFactor = value.GetValueOrDefault(); + SetFlag(VsimFlags.SearchExplorationFactor, true); + } + else + { + SetFlag(VsimFlags.SearchExplorationFactor, false); + } + } + } + + /// + /// Optional maximum filtering attempts (FILTER-EF parameter). + /// + public int? MaxFilteringEffort + { + get => HasFlag(VsimFlags.MaxFilteringEffort) ? _maxFilteringEffort : null; + set + { + if (value.HasValue) + { + _maxFilteringEffort = value.GetValueOrDefault(); + SetFlag(VsimFlags.MaxFilteringEffort, true); + } + else + { + SetFlag(VsimFlags.MaxFilteringEffort, false); + } + } + } + + private string? _filterExpression; + + /// + /// Optional filter expression to restrict results (FILTER parameter); . + /// + public string? FilterExpression + { + get => _filterExpression; + set + { + _filterExpression = value; + SetFlag(VsimFlags.FilterExpression, !string.IsNullOrWhiteSpace(value)); + } + } + + /// + /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). + /// + public bool UseExactSearch + { + get => HasFlag(VsimFlags.UseExactSearch); + set => SetFlag(VsimFlags.UseExactSearch, value); + } + + /// + /// Whether to run search in main thread (NOTHREAD parameter). + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)] + public bool DisableThreading + { + get => HasFlag(VsimFlags.DisableThreading); + set => SetFlag(VsimFlags.DisableThreading, value); + } + + // snapshot the values; I don't trust people not to mutate the object behind my back + internal VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new( + db, + flags, + _vsimFlags, + key, + Member, + Vector, + _count, + _epsilon, + _searchExplorationFactor, + _filterExpression, + _maxFilteringEffort); +} diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs index b3cff51a5..4396e0044 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -171,42 +171,41 @@ public void VectorSetSimilaritySearchByVector() { var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); - prefixed.VectorSetSimilaritySearchByVector( + var query = new VectorSetSimilaritySearchRequest + { + Vector = vector, + }; + prefixed.VectorSetSimilaritySearch( "vectorset", - vector); - mock.Received().VectorSetSimilaritySearchByVector( + query); + mock.Received().VectorSetSimilaritySearch( "prefix:vectorset", - vector); + query); } [Fact] public void VectorSetSimilaritySearchByMember() { - prefixed.VectorSetSimilaritySearchByMember( + var query = new VectorSetSimilaritySearchRequest + { + Member = "member1", + Count = 5, + WithScores = true, + WithAttributes = true, + Epsilon = 0.1, + SearchExplorationFactor = 400, + FilterExpression = "category='test'", + MaxFilteringEffort = 1000, + UseExactSearch = true, + DisableThreading = true, + }; + prefixed.VectorSetSimilaritySearch( "vectorset", - "member1", - 5, - true, - true, - 0.1, - 400, - "category='test'", - 1000, - true, - true, + query, CommandFlags.FireAndForget); - mock.Received().VectorSetSimilaritySearchByMember( + mock.Received().VectorSetSimilaritySearch( "prefix:vectorset", - "member1", - 5, - true, - true, - 0.1, - 400, - "category='test'", - 1000, - true, - true, + query, CommandFlags.FireAndForget); } @@ -216,10 +215,14 @@ public void VectorSetSimilaritySearchByVector_DefaultParameters() var vector = new[] { 1.0f, 2.0f }.AsMemory(); // Test that default parameters work correctly - prefixed.VectorSetSimilaritySearchByVector("vectorset", vector); - mock.Received().VectorSetSimilaritySearchByVector( + var query = new VectorSetSimilaritySearchRequest + { + Vector = vector, + }; + prefixed.VectorSetSimilaritySearch("vectorset", query); + mock.Received().VectorSetSimilaritySearch( "prefix:vectorset", - vector); + query); } } } diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 330407f70..449313669 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -20,7 +20,7 @@ public async Task VectorSetAdd_BasicOperation(bool suppressFp32) var key = Me(); // Clean up any existing data - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; @@ -44,7 +44,7 @@ public async Task VectorSetAdd_WithAttributes() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; var attributes = """{"category":"test","id":123}"""; @@ -68,7 +68,7 @@ public async Task VectorSetAdd_WithEverything(VectorSetQuantization quantization var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; var attributes = """{"category":"test","id":123}"""; @@ -98,7 +98,7 @@ public async Task VectorSetLength_EmptySet() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var length = await db.VectorSetLengthAsync(key); Assert.Equal(0, length); @@ -111,7 +111,7 @@ public async Task VectorSetLength_WithElements() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; @@ -130,7 +130,7 @@ public async Task VectorSetDimension() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); @@ -148,7 +148,7 @@ public async Task VectorSetContains(bool suppressFp32) var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f }; if (suppressFp32) VectorSetAddMessage.SuppressFp32(); @@ -177,7 +177,7 @@ public async Task VectorSetGetApproximateVector(bool suppressFp32) var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var originalVector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; if (suppressFp32) VectorSetAddMessage.SuppressFp32(); @@ -212,7 +212,7 @@ public async Task VectorSetRemove() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); @@ -237,7 +237,7 @@ public async Task VectorSetInfo(VectorSetQuantization quantization) var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), quantization: quantization); @@ -262,7 +262,7 @@ public async Task VectorSetRandomMember() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; @@ -281,7 +281,7 @@ public async Task VectorSetRandomMembers() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; @@ -310,7 +310,7 @@ public async Task VectorSetSimilaritySearch_ByVector(bool withScores, bool withA var disambiguator = (withScores ? 1 : 0) + (withAttributes ? 2 : 0); var key = Me() + disambiguator; - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); // Add some test vectors var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; @@ -323,12 +323,12 @@ public async Task VectorSetSimilaritySearch_ByVector(bool withScores, bool withA // Search for vectors similar to vector1 using var results = - await db.VectorSetSimilaritySearchByVectorAsync( + await db.VectorSetSimilaritySearchAsync( key, - vector1.AsMemory(), - count: 2, - withScores: withScores, - withAttributes: withAttributes); + new() + { + Vector = vector1.AsMemory(), Count = 2, WithScores = withScores, WithAttributes = withAttributes, + }); Assert.NotNull(results); foreach (var result in results.Span) @@ -366,7 +366,7 @@ public async Task VectorSetSimilaritySearch_ByMember(bool withScores, bool withA var disambiguator = (withScores ? 1 : 0) + (withAttributes ? 2 : 0); var key = Me() + disambiguator; - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; @@ -375,12 +375,12 @@ public async Task VectorSetSimilaritySearch_ByMember(bool withScores, bool withA await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); using var results = - await db.VectorSetSimilaritySearchByMemberAsync( + await db.VectorSetSimilaritySearchAsync( key, - "element1", - count: 1, - withScores: withScores, - withAttributes: withAttributes); + new() + { + Member = "element1", Count = 1, WithScores = withScores, WithAttributes = withAttributes, + }); Assert.NotNull(results); foreach (var result in results.Span) @@ -415,7 +415,7 @@ public async Task VectorSetSimilaritySearch_WithFilter(bool corruptPrefix, bool var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); Random rand = new Random(); @@ -443,13 +443,16 @@ void ScrambleVector() ScrambleVector(); using var results = - await db.VectorSetSimilaritySearchByVectorAsync( + await db.VectorSetSimilaritySearchAsync( key, - vector, - count: 100, - withScores: true, - withAttributes: true, - filterExpression: ".id >= 30"); + new() + { + Vector = vector, + Count = 100, + WithScores = true, + WithAttributes = true, + FilterExpression = ".id >= 30", + }); Assert.NotNull(results); foreach (var result in results.Span) @@ -484,6 +487,65 @@ static int GetId(string member) } } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(".id >= 30")] + public async Task VectorSetSimilaritySearch_TestFilterValues(string? filterExpression) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + Random rand = new Random(); + + float[] vector = new float[50]; + + void ScrambleVector() + { + var arr = vector; + for (int i = 0; i < arr.Length; i++) + { + arr[i] = (float)rand.NextDouble(); + } + } + + string[] regions = new[] { "us-west", "us-east", "eu-west", "eu-east", "ap-south", "ap-north" }; + for (int i = 0; i < 100; i++) + { + var region = regions[rand.Next(regions.Length)]; + var json = JsonConvert.SerializeObject(new { id = i, region }); + ScrambleVector(); + await db.VectorSetAddAsync(key, $"element{i}", vector, attributesJson: json); + } + + ScrambleVector(); + using var results = + await db.VectorSetSimilaritySearchAsync( + key, + new() + { + Vector = vector, + Count = 100, + WithScores = true, + WithAttributes = true, + FilterExpression = filterExpression, + }); + + Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } + + Log($"Total matches: {results.Span.Length}"); + // we're not interested in the specific results; we're just checking that the + // filter expression was added and parsed without exploding about arg mismatch + } + [Fact] public async Task VectorSetSetAttributesJson() { @@ -491,7 +553,7 @@ public async Task VectorSetSetAttributesJson() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new float[] { 1.0f, 2.0f, 3.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); @@ -518,7 +580,7 @@ public async Task VectorSetGetLinks() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); // Add some vectors that should be linked var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; @@ -552,7 +614,7 @@ public async Task VectorSetGetLinksWithScores() var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); // Add some vectors with known relationships var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; From 2157db11834474d440836d74d8798a95b327d414 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 9 Sep 2025 10:15:19 +0100 Subject: [PATCH 27/32] change VSIM API to allow future extensivility: - make VectorSetSimilaritySearchRequest abstract - remove Member and Vector - add ByMember and ByVector factory methods - (internal changes to support the above, in the message etc) --- .../PublicAPI/PublicAPI.Unshipped.txt | 7 +- .../VectorSetSimilaritySearchMessage.cs | 117 ++++++++++++------ .../VectorSetSimilaritySearchRequest.cs | 73 ++++++++--- .../KeyPrefixedVectorSetTests.cs | 33 ++--- .../VectorSetIntegrationTests.cs | 116 ++++++++--------- 5 files changed, 198 insertions(+), 148 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 3ac3b0455..9f25d568c 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -10,15 +10,10 @@ [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.set -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.get -> int? [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Member.get -> StackExchange.Redis.RedisValue -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Member.set -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.get -> int? [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.set -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.get -> bool [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Vector.get -> System.ReadOnlyMemory -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Vector.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.VectorSetSimilaritySearchRequest() -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.get -> bool [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.set -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.get -> bool @@ -79,3 +74,5 @@ [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Score.get -> double [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult() -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void +[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! +[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs index 45e0a27d8..1bbc418d5 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs @@ -2,20 +2,85 @@ namespace StackExchange.Redis; -internal sealed class VectorSetSimilaritySearchMessage( +internal abstract class VectorSetSimilaritySearchMessage( int db, CommandFlags flags, VectorSetSimilaritySearchMessage.VsimFlags vsimFlags, RedisKey key, - RedisValue member, - ReadOnlyMemory vector, int count, double epsilon, int searchExplorationFactor, string? filterExpression, int maxFilteringEffort) : Message(db, flags, RedisCommand.VSIM) { - public ResultProcessor?> GetResultProcessor() => VectorSetSimilaritySearchProcessor.Instance; + // For "FP32" and "VALUES" scenarios; in the future we might want other vector sizes / encodings - for + // example, there could be some "FP16" or "FP8" transport that requires a ROM-short or ROM-sbyte from + // the calling code. Or, as a convenience, we might want to allow ROM-double input, but transcode that + // to FP32 on the way out. + internal sealed class VectorSetSimilaritySearchBySingleVectorMessage( + int db, + CommandFlags flags, + VsimFlags vsimFlags, + RedisKey key, + ReadOnlyMemory vector, + int count, + double epsilon, + int searchExplorationFactor, + string? filterExpression, + int maxFilteringEffort) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon, + searchExplorationFactor, filterExpression, maxFilteringEffort) + { + internal override int GetSearchTargetArgCount(bool packed) => + packed ? 2 : 2 + vector.Length; // FP32 {vector} or VALUES {num} {vector} + + internal override void WriteSearchTarget(bool packed, PhysicalConnection physical) + { + if (packed) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(System.Runtime.InteropServices.MemoryMarshal.AsBytes(vector.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(vector.Length); + foreach (var val in vector.Span) + { + physical.WriteBulkString(val); + } + } + } + } + + // for "ELE" scenarios + internal sealed class VectorSetSimilaritySearchByMemberMessage( + int db, + CommandFlags flags, + VsimFlags vsimFlags, + RedisKey key, + RedisValue member, + int count, + double epsilon, + int searchExplorationFactor, + string? filterExpression, + int maxFilteringEffort) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon, + searchExplorationFactor, filterExpression, maxFilteringEffort) + { + internal override int GetSearchTargetArgCount(bool packed) => 2; // ELE {member} + + internal override void WriteSearchTarget(bool packed, PhysicalConnection physical) + { + physical.WriteBulkString("ELE"u8); + physical.WriteBulkString(member); + } + } + + internal abstract int GetSearchTargetArgCount(bool packed); + internal abstract void WriteSearchTarget(bool packed, PhysicalConnection physical); + + public ResultProcessor?> GetResultProcessor() => + VectorSetSimilaritySearchProcessor.Instance; + private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> { // keep local, since we need to know what flags were being sent @@ -38,7 +103,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // in RESP3 mode (only), when both are requested, we get a sub-array per item; weird, but true bool internalNesting = withScores && withAttribs && connection.Protocol is RedisProtocol.Resp3; - int rowsPerItem = internalNesting ? 2 + int rowsPerItem = internalNesting + ? 2 : 1 + ((withScores ? 1 : 0) + (withAttribs ? 1 : 0)); // each value is separate root element var items = result.GetItems(); @@ -80,11 +146,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes target[i] = new VectorSetSimilaritySearchResult(member, score, attributesJson); count++; } + if (count == target.Length) { SetResult(message, lease); return true; } + lease.Dispose(); // failed to fill? } @@ -111,14 +179,9 @@ internal enum VsimFlags public override int ArgCount => GetArgCount(VectorSetAddMessage.UseFp32); - private int GetArgCount(bool useFp32) + private int GetArgCount(bool packed) { - int argCount = 3; // {key} and "ELE {member}", "FP32 {vector}" or "VALUES {num}" - if (member.IsNull && !useFp32) - { - argCount += vector.Length; // {vector} in the VALUES case - } - + int argCount = 1 + GetSearchTargetArgCount(packed); // {key} and whatever we need for the vector/element portion if (HasFlag(VsimFlags.WithScores)) argCount++; // [WITHSCORES] if (HasFlag(VsimFlags.WithAttributes)) argCount++; // [WITHATTRIBS] if (HasFlag(VsimFlags.Count)) argCount += 2; // [COUNT {count}] @@ -133,37 +196,15 @@ private int GetArgCount(bool useFp32) protected override void WriteImpl(PhysicalConnection physical) { - var useFp32 = VectorSetAddMessage.UseFp32; // avoid race in debug mode - physical.WriteHeader(Command, GetArgCount(useFp32)); + // snapshot to avoid race in debug scenarios + bool packed = VectorSetAddMessage.UseFp32; + physical.WriteHeader(Command, GetArgCount(packed)); // Write key physical.Write(key); // Write search target: either "ELE {member}" or vector data - if (!member.IsNull) - { - // Member-based search: "ELE {member}" - physical.WriteBulkString("ELE"u8); - physical.WriteBulkString(member); - } - else - { - // Vector-based search: either "FP32 {vector}" or "VALUES {num} {vector}" - if (useFp32) - { - physical.WriteBulkString("FP32"u8); - physical.WriteBulkString(System.Runtime.InteropServices.MemoryMarshal.AsBytes(vector.Span)); - } - else - { - physical.WriteBulkString("VALUES"u8); - physical.WriteBulkString(vector.Length); - foreach (var val in vector.Span) - { - physical.WriteBulkString(val); - } - } - } + WriteSearchTarget(packed, physical); if (HasFlag(VsimFlags.WithScores)) { diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs index 9480a8120..a4796ed75 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs @@ -9,17 +9,65 @@ namespace StackExchange.Redis; /// Represents the request for a vector similarity search operation. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] -public class VectorSetSimilaritySearchRequest +public abstract class VectorSetSimilaritySearchRequest { + internal VectorSetSimilaritySearchRequest() + { + } // polymorphism left open for future, but needs to be handled internally + + private sealed class VectorSetSimilarityByMemberSearchRequest(RedisValue member) : VectorSetSimilaritySearchRequest + { + public RedisValue Member => member; + + internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new VectorSetSimilaritySearchMessage.VectorSetSimilaritySearchByMemberMessage( + db, + flags, + _vsimFlags, + key, + Member, + _count, + _epsilon, + _searchExplorationFactor, + _filterExpression, + _maxFilteringEffort); + } + + private sealed class VectorSetSimilarityVectorSingleSearchRequest(ReadOnlyMemory vector) + : VectorSetSimilaritySearchRequest + { + public ReadOnlyMemory Vector => vector; + + internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new VectorSetSimilaritySearchMessage.VectorSetSimilaritySearchBySingleVectorMessage( + db, + flags, + _vsimFlags, + key, + Vector, + _count, + _epsilon, + _searchExplorationFactor, + _filterExpression, + _maxFilteringEffort); + } + + // snapshot the values; I don't trust people not to mutate the object behind my back + internal abstract VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags); + /// - /// The query vector. + /// Create a request to search by an existing member in the index. /// - public ReadOnlyMemory Vector { get; set; } + /// The member to search for. + public static VectorSetSimilaritySearchRequest ByMember(RedisValue member) + => new VectorSetSimilarityByMemberSearchRequest(member); /// - /// The member to find similar vectors for. + /// Create a request to search by a vector value. /// - public RedisValue Member { get; set; } + /// The vector value to search for. + public static VectorSetSimilaritySearchRequest ByVector(ReadOnlyMemory vector) + => new VectorSetSimilarityVectorSingleSearchRequest(vector); private VsimFlags _vsimFlags; @@ -172,19 +220,4 @@ public bool DisableThreading get => HasFlag(VsimFlags.DisableThreading); set => SetFlag(VsimFlags.DisableThreading, value); } - - // snapshot the values; I don't trust people not to mutate the object behind my back - internal VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) - => new( - db, - flags, - _vsimFlags, - key, - Member, - Vector, - _count, - _epsilon, - _searchExplorationFactor, - _filterExpression, - _maxFilteringEffort); } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs index 4396e0044..f73041186 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -171,10 +171,7 @@ public void VectorSetSimilaritySearchByVector() { var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); - var query = new VectorSetSimilaritySearchRequest - { - Vector = vector, - }; + var query = VectorSetSimilaritySearchRequest.ByVector(vector); prefixed.VectorSetSimilaritySearch( "vectorset", query); @@ -186,19 +183,16 @@ public void VectorSetSimilaritySearchByVector() [Fact] public void VectorSetSimilaritySearchByMember() { - var query = new VectorSetSimilaritySearchRequest - { - Member = "member1", - Count = 5, - WithScores = true, - WithAttributes = true, - Epsilon = 0.1, - SearchExplorationFactor = 400, - FilterExpression = "category='test'", - MaxFilteringEffort = 1000, - UseExactSearch = true, - DisableThreading = true, - }; + var query = VectorSetSimilaritySearchRequest.ByMember("member1"); + query.Count = 5; + query.WithScores = true; + query.WithAttributes = true; + query.Epsilon = 0.1; + query.SearchExplorationFactor = 400; + query.FilterExpression = "category='test'"; + query.MaxFilteringEffort = 1000; + query.UseExactSearch = true; + query.DisableThreading = true; prefixed.VectorSetSimilaritySearch( "vectorset", query, @@ -215,10 +209,7 @@ public void VectorSetSimilaritySearchByVector_DefaultParameters() var vector = new[] { 1.0f, 2.0f }.AsMemory(); // Test that default parameters work correctly - var query = new VectorSetSimilaritySearchRequest - { - Vector = vector, - }; + var query = VectorSetSimilaritySearchRequest.ByVector(vector); prefixed.VectorSetSimilaritySearch("vectorset", query); mock.Received().VectorSetSimilaritySearch( "prefix:vectorset", diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 449313669..c71fa4359 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -22,7 +22,7 @@ public async Task VectorSetAdd_BasicOperation(bool suppressFp32) // Clean up any existing data await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; if (suppressFp32) VectorSetAddMessage.SuppressFp32(); try @@ -46,7 +46,7 @@ public async Task VectorSetAdd_WithAttributes() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; var attributes = """{"category":"test","id":123}"""; var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), attributesJson: attributes); @@ -70,7 +70,7 @@ public async Task VectorSetAdd_WithEverything(VectorSetQuantization quantization await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; var attributes = """{"category":"test","id":123}"""; var result = await db.VectorSetAddAsync( @@ -113,8 +113,8 @@ public async Task VectorSetLength_WithElements() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; - var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; + var vector1 = new[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new[] { 4.0f, 5.0f, 6.0f }; await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); @@ -132,7 +132,7 @@ public async Task VectorSetDimension() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); var dimension = await db.VectorSetDimensionAsync(key); @@ -150,7 +150,7 @@ public async Task VectorSetContains(bool suppressFp32) await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f }; if (suppressFp32) VectorSetAddMessage.SuppressFp32(); try { @@ -179,7 +179,7 @@ public async Task VectorSetGetApproximateVector(bool suppressFp32) await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var originalVector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var originalVector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; if (suppressFp32) VectorSetAddMessage.SuppressFp32(); try { @@ -214,7 +214,7 @@ public async Task VectorSetRemove() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); var removed = await db.VectorSetRemoveAsync(key, "element1"); @@ -239,7 +239,7 @@ public async Task VectorSetInfo(VectorSetQuantization quantization) await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), quantization: quantization); var info = await db.VectorSetInfoAsync(key); @@ -264,8 +264,8 @@ public async Task VectorSetRandomMember() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; - var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; + var vector1 = new[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new[] { 4.0f, 5.0f, 6.0f }; await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); @@ -283,9 +283,9 @@ public async Task VectorSetRandomMembers() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector1 = new float[] { 1.0f, 2.0f, 3.0f }; - var vector2 = new float[] { 4.0f, 5.0f, 6.0f }; - var vector3 = new float[] { 7.0f, 8.0f, 9.0f }; + var vector1 = new[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new[] { 4.0f, 5.0f, 6.0f }; + var vector3 = new[] { 7.0f, 8.0f, 9.0f }; await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); @@ -313,22 +313,20 @@ public async Task VectorSetSimilaritySearch_ByVector(bool withScores, bool withA await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); // Add some test vectors - var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; - var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; - var vector3 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.0f, 1.0f, 0.0f }; + var vector3 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 await db.VectorSetAddAsync(key, "element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); await db.VectorSetAddAsync(key, "element3", vector3.AsMemory(), attributesJson: """{"category":"z"}"""); // Search for vectors similar to vector1 - using var results = - await db.VectorSetSimilaritySearchAsync( - key, - new() - { - Vector = vector1.AsMemory(), Count = 2, WithScores = withScores, WithAttributes = withAttributes, - }); + var query = VectorSetSimilaritySearchRequest.ByVector(vector1.AsMemory()); + query.Count = 2; + query.WithScores = withScores; + query.WithAttributes = withAttributes; + using var results = await db.VectorSetSimilaritySearchAsync(key, query); Assert.NotNull(results); foreach (var result in results.Span) @@ -368,19 +366,17 @@ public async Task VectorSetSimilaritySearch_ByMember(bool withScores, bool withA await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; - var vector2 = new float[] { 0.0f, 1.0f, 0.0f }; + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.0f, 1.0f, 0.0f }; await db.VectorSetAddAsync(key, "element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); - using var results = - await db.VectorSetSimilaritySearchAsync( - key, - new() - { - Member = "element1", Count = 1, WithScores = withScores, WithAttributes = withAttributes, - }); + var query = VectorSetSimilaritySearchRequest.ByMember("element1"); + query.Count = 1; + query.WithScores = withScores; + query.WithAttributes = withAttributes; + using var results = await db.VectorSetSimilaritySearchAsync(key, query); Assert.NotNull(results); foreach (var result in results.Span) @@ -442,17 +438,12 @@ void ScrambleVector() } ScrambleVector(); - using var results = - await db.VectorSetSimilaritySearchAsync( - key, - new() - { - Vector = vector, - Count = 100, - WithScores = true, - WithAttributes = true, - FilterExpression = ".id >= 30", - }); + var query = VectorSetSimilaritySearchRequest.ByVector(vector); + query.Count = 100; + query.WithScores = true; + query.WithAttributes = true; + query.FilterExpression = ".id >= 30"; + using var results = await db.VectorSetSimilaritySearchAsync(key, query); Assert.NotNull(results); foreach (var result in results.Span) @@ -523,17 +514,13 @@ void ScrambleVector() } ScrambleVector(); - using var results = - await db.VectorSetSimilaritySearchAsync( - key, - new() - { - Vector = vector, - Count = 100, - WithScores = true, - WithAttributes = true, - FilterExpression = filterExpression, - }); + var query = VectorSetSimilaritySearchRequest.ByVector(vector); + query.Count = 100; + query.WithScores = true; + query.WithAttributes = true; + query.FilterExpression = filterExpression; + + using var results = await db.VectorSetSimilaritySearchAsync(key, query); Assert.NotNull(results); foreach (var result in results.Span) @@ -555,7 +542,7 @@ public async Task VectorSetSetAttributesJson() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - var vector = new float[] { 1.0f, 2.0f, 3.0f }; + var vector = new[] { 1.0f, 2.0f, 3.0f }; await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); // Set attributes for existing element @@ -583,9 +570,9 @@ public async Task VectorSetGetLinks() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); // Add some vectors that should be linked - var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; - var vector2 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 - var vector3 = new float[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector3 = new[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); @@ -617,9 +604,9 @@ public async Task VectorSetGetLinksWithScores() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); // Add some vectors with known relationships - var vector1 = new float[] { 1.0f, 0.0f, 0.0f }; - var vector2 = new float[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 - var vector3 = new float[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector3 = new[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); @@ -637,7 +624,8 @@ public async Task VectorSetGetLinksWithScores() Assert.NotEmpty(linksArray); // Verify each link has a valid score - Assert.All(linksArray, link => + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + Assert.All(linksArray, static link => { Assert.False(link.Member.IsNull); Assert.False(double.IsNaN(link.Score)); From a53365b0a6534b5eebefdd757e98f5a94450f583 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 9 Sep 2025 10:22:14 +0100 Subject: [PATCH 28/32] remove redundant property --- .../VectorSetSimilaritySearchRequest.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs index a4796ed75..d0c0fd4cc 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs @@ -17,15 +17,13 @@ internal VectorSetSimilaritySearchRequest() private sealed class VectorSetSimilarityByMemberSearchRequest(RedisValue member) : VectorSetSimilaritySearchRequest { - public RedisValue Member => member; - internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) => new VectorSetSimilaritySearchMessage.VectorSetSimilaritySearchByMemberMessage( db, flags, _vsimFlags, key, - Member, + member, _count, _epsilon, _searchExplorationFactor, @@ -36,15 +34,13 @@ internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int d private sealed class VectorSetSimilarityVectorSingleSearchRequest(ReadOnlyMemory vector) : VectorSetSimilaritySearchRequest { - public ReadOnlyMemory Vector => vector; - internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) => new VectorSetSimilaritySearchMessage.VectorSetSimilaritySearchBySingleVectorMessage( db, flags, _vsimFlags, key, - Vector, + vector, _count, _epsilon, _searchExplorationFactor, From 65efb51a05c0b4a6f48d93d3f6e5e2a46925aa7d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 10:00:21 +0100 Subject: [PATCH 29/32] refactor VADD API: - new abstract VectorSetAddRequest with core props - VectorSetAddRequest.Member for adding single element - change VectorSetAdd[Async] to take VectorSetAddRequest --- .../Interfaces/IDatabase.VectorSets.cs | 18 +-- .../Interfaces/IDatabaseAsync.VectorSets.cs | 11 +- .../KeyPrefixed.VectorSets.cs | 11 +- .../KeyPrefixedDatabase.VectorSets.cs | 11 +- .../PublicAPI/PublicAPI.Unshipped.txt | 20 ++- .../RedisDatabase.VectorSets.cs | 22 +-- .../VectorSetAddMessage.cs | 153 ++++++++++++------ .../VectorSetAddRequest.cs | 80 +++++++++ .../VectorSetQuantization.cs | 8 +- .../KeyPrefixedVectorSetTests.cs | 33 ++-- .../VectorSetIntegrationTests.cs | 105 +++++++----- 11 files changed, 296 insertions(+), 176 deletions(-) create mode 100644 src/StackExchange.Redis/VectorSetAddRequest.cs diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs index 741f968ab..7cad6e9b5 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -15,28 +15,14 @@ public partial interface IDatabase /// Add a vector to a vectorset. /// /// The key of the vectorset. - /// The element name. - /// The vector data. - /// Optional dimension reduction using random projection (REDUCE parameter). - /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. - /// Optional HNSW build exploration factor (EF parameter, default: 200). - /// Optional maximum connections per HNSW node (M parameter, default: 16). - /// Optional check-and-set mode for partial threading (CAS parameter). - /// Optional JSON attributes for the element (SETATTR parameter). + /// The data to add. /// The flags to use for this operation. /// if the element was added; if it already existed. /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] bool VectorSetAdd( RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, + VectorSetAddRequest request, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs index de60b2a3b..91f4b1863 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -12,18 +12,11 @@ public partial interface IDatabaseAsync { // Vector Set operations - /// + /// [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] Task VectorSetAddAsync( RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, + VectorSetAddRequest request, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs index 403a38396..7d116c05f 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -11,16 +11,9 @@ internal partial class KeyPrefixed [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] public Task VectorSetAddAsync( RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, + VectorSetAddRequest request, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetAddAsync(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + Inner.VectorSetAddAsync(ToInner(key), request, flags); public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.VectorSetLengthAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs index 9c24961d1..a5bedfdf9 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs @@ -8,16 +8,9 @@ internal sealed partial class KeyPrefixedDatabase // Vector Set operations public bool VectorSetAdd( RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, + VectorSetAddRequest request, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetAdd(ToInner(key), element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson, flags); + Inner.VectorSetAdd(ToInner(key), request, flags); public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.VectorSetLength(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 9f25d568c..510525a03 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,19 @@ #nullable enable +[SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! +[SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! +[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.VectorSetAddRequest +[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.get -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.get -> bool +[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.set -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.get -> int? [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.set -> void @@ -18,9 +33,6 @@ [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.set -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.get -> bool [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.set -> void -[SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! -[SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! -[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int [SER001]StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? @@ -34,7 +46,6 @@ [SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, int? reducedDimensions = null, StackExchange.Redis.VectorSetQuantization quantization = StackExchange.Redis.VectorSetQuantization.Int8, int? buildExplorationFactor = null, int? maxConnections = null, bool useCheckAndSet = false, string? attributesJson = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! @@ -74,5 +85,6 @@ [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Score.get -> double [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult() -> void [SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void +[SER001]static StackExchange.Redis.VectorSetAddRequest.Member(StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, string? attributesJson = null) -> StackExchange.Redis.VectorSetAddRequest! [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs index 04ec9f2c7..4340de3db 100644 --- a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -8,17 +8,10 @@ internal partial class RedisDatabase { public bool VectorSetAdd( RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, + VectorSetAddRequest request, CommandFlags flags = CommandFlags.None) { - var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + var msg = request.ToMessage(key, Database, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } @@ -107,17 +100,10 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string j // Vector Set async operations public Task VectorSetAddAsync( RedisKey key, - RedisValue element, - ReadOnlyMemory values, - int? reducedDimensions = null, - VectorSetQuantization quantization = VectorSetQuantization.Int8, - int? buildExplorationFactor = null, - int? maxConnections = null, - bool useCheckAndSet = false, - string? attributesJson = null, + VectorSetAddRequest request, CommandFlags flags = CommandFlags.None) { - var msg = new VectorSetAddMessage(Database, flags, key, element, values, reducedDimensions, quantization, buildExplorationFactor, maxConnections, useCheckAndSet, attributesJson); + var msg = request.ToMessage(key, Database, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs index 26bd739be..0beb65205 100644 --- a/src/StackExchange.Redis/VectorSetAddMessage.cs +++ b/src/StackExchange.Redis/VectorSetAddMessage.cs @@ -4,45 +4,23 @@ namespace StackExchange.Redis; -internal sealed class VectorSetAddMessage( - int database, +internal abstract class VectorSetAddMessage( + int db, CommandFlags flags, RedisKey key, - RedisValue element, - ReadOnlyMemory values, int? reducedDimensions, VectorSetQuantization quantization, int? buildExplorationFactor, int? maxConnections, - bool useCheckAndSet, - string? attributesJson) : Message(database, flags, RedisCommand.VADD) + bool useCheckAndSet) : Message(db, flags, RedisCommand.VADD) { - private static readonly bool CanUseFp32 = BitConverter.IsLittleEndian && CheckFp32(); - private static bool CheckFp32() // check endianness with a known value - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - expect exact - return MemoryMarshal.Cast("\0\0(B"u8)[0] == 42; - } -#if DEBUG - private static int _fp32Disabled; - internal static bool UseFp32 => CanUseFp32 & Volatile.Read(ref _fp32Disabled) == 0; - internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled); - internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled); -#else - internal static bool UseFp32 => CanUseFp32; - internal static void SuppressFp32() { } - internal static void RestoreFp32() { } -#endif - public override int ArgCount => GetArgCount(UseFp32); - private int GetArgCount(bool useFp32) + private int GetArgCount(bool packed) { - var count = 4; // key, element and either "FP32 {vector}" or VALUES {num}" + var count = 2 + GetElementArgCount(packed); // key, element and either "FP32 {vector}" or VALUES {num}" if (reducedDimensions.HasValue) count += 2; // [REDUCE {dim}] - if (!useFp32) count += values.Length; // {vector} in the VALUES case - if (useCheckAndSet) count++; // [CAS] count += quantization switch { @@ -52,36 +30,50 @@ private int GetArgCount(bool useFp32) }; if (buildExplorationFactor.HasValue) count += 2; // [EF {build-exploration-factor}] - if (attributesJson is not null) count += 2; // [SETATTR {attributes}] + count += GetAttributeArgCount(); // [SETATTR {attributes}] if (maxConnections.HasValue) count += 2; // [M {numlinks}] return count; } + public abstract int GetElementArgCount(bool packed); + public abstract int GetAttributeArgCount(); + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); + + private static readonly bool CanUseFp32 = BitConverter.IsLittleEndian && CheckFp32(); + + private static bool CheckFp32() // check endianness with a known value + { + // ReSharper disable once CompareOfFloatsByEqualityOperator - expect exact + return MemoryMarshal.Cast("\0\0(B"u8)[0] == 42; + } + +#if DEBUG + private static int _fp32Disabled; + internal static bool UseFp32 => CanUseFp32 & Volatile.Read(ref _fp32Disabled) == 0; + internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled); + internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled); +#else + internal static bool UseFp32 => CanUseFp32; + internal static void SuppressFp32() { } + internal static void RestoreFp32() { } +#endif + + protected abstract void WriteElement(bool packed, PhysicalConnection physical); + protected override void WriteImpl(PhysicalConnection physical) { - bool useFp32 = UseFp32; // snapshot to avoid race in debug scenarios - physical.WriteHeader(Command, GetArgCount(useFp32)); + bool packed = UseFp32; // snapshot to avoid race in debug scenarios + physical.WriteHeader(Command, GetArgCount(packed)); physical.Write(key); if (reducedDimensions.HasValue) { physical.WriteBulkString("REDUCE"u8); physical.WriteBulkString(reducedDimensions.GetValueOrDefault()); } - if (useFp32) - { - physical.WriteBulkString("FP32"u8); - physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span)); - } - else - { - physical.WriteBulkString("VALUES"u8); - physical.WriteBulkString(values.Length); - foreach (var val in values.Span) - { - physical.WriteBulkString(val); - } - } - physical.WriteBulkString(element); + + WriteElement(packed, physical); if (useCheckAndSet) physical.WriteBulkString("CAS"u8); switch (quantization) @@ -97,16 +89,15 @@ protected override void WriteImpl(PhysicalConnection physical) default: throw new ArgumentOutOfRangeException(nameof(quantization)); } + if (buildExplorationFactor.HasValue) { physical.WriteBulkString("EF"u8); physical.WriteBulkString(buildExplorationFactor.GetValueOrDefault()); } - if (attributesJson is not null) - { - physical.WriteBulkString("SETATTR"u8); - physical.WriteBulkString(attributesJson); - } + + WriteAttributes(physical); + if (maxConnections.HasValue) { physical.WriteBulkString("M"u8); @@ -114,6 +105,64 @@ protected override void WriteImpl(PhysicalConnection physical) } } - public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - => serverSelectionStrategy.HashSlot(key); + protected abstract void WriteAttributes(PhysicalConnection physical); + + internal sealed class VectorSetAddMemberMessage( + int db, + CommandFlags flags, + RedisKey key, + int? reducedDimensions, + VectorSetQuantization quantization, + int? buildExplorationFactor, + int? maxConnections, + bool useCheckAndSet, + RedisValue element, + ReadOnlyMemory values, + string? attributesJson) : VectorSetAddMessage( + db, + flags, + key, + reducedDimensions, + quantization, + buildExplorationFactor, + maxConnections, + useCheckAndSet) + { + private readonly string? _attributesJson = string.IsNullOrWhiteSpace(attributesJson) ? null : attributesJson; + public override int GetElementArgCount(bool packed) + => 2 // "FP32 {vector}" or "VALUES {num}" + + (packed ? 0 : values.Length); // {vector...}" + + public override int GetAttributeArgCount() + => _attributesJson is null ? 0 : 2; // [SETATTR {attributes}] + + protected override void WriteElement(bool packed, PhysicalConnection physical) + { + if (packed) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(values.Length); + foreach (var val in values.Span) + { + physical.WriteBulkString(val); + } + } + + physical.WriteBulkString(element); + } + + protected override void WriteAttributes(PhysicalConnection physical) + { + if (_attributesJson is not null) + { + physical.WriteBulkString("SETATTR"u8); + physical.WriteBulkString(_attributesJson); + } + } + } } diff --git a/src/StackExchange.Redis/VectorSetAddRequest.cs b/src/StackExchange.Redis/VectorSetAddRequest.cs new file mode 100644 index 000000000..987118c09 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetAddRequest.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Represents the request for a vectorset add operation. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public abstract class VectorSetAddRequest +{ + // polymorphism left open for future, but needs to be handled internally + internal VectorSetAddRequest() + { + } + + /// + /// Add a member to the vectorset. + /// + /// The element name. + /// The vector data. + /// Optional JSON attributes for the element (SETATTR parameter). + public static VectorSetAddRequest Member( + RedisValue element, + ReadOnlyMemory values, +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + string? attributesJson = null) + => new VectorSetAddMemberRequest(element, values, attributesJson); + + /// + /// Optional check-and-set mode for partial threading (CAS parameter). + /// + public bool UseCheckAndSet { get; set; } + + /// + /// Optional dimension reduction using random projection (REDUCE parameter). + /// + public int? ReducedDimensions { get; set; } + + /// + /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. + /// + public VectorSetQuantization Quantization { get; set; } = VectorSetQuantization.Int8; + + /// + /// Optional HNSW build exploration factor (EF parameter, default: 200). + /// + public int? BuildExplorationFactor { get; set; } + + /// + /// Optional maximum connections per HNSW node (M parameter, default: 16). + /// + public int? MaxConnections { get; set; } + + // snapshot the values; I don't trust people not to mutate the object behind my back + internal abstract VectorSetAddMessage ToMessage(RedisKey key, int db, CommandFlags flags); + + internal sealed class VectorSetAddMemberRequest( + RedisValue element, + ReadOnlyMemory values, + string? attributesJson) + : VectorSetAddRequest + { + internal override VectorSetAddMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new VectorSetAddMessage.VectorSetAddMemberMessage( + db, + flags, + key, + ReducedDimensions, + Quantization, + BuildExplorationFactor, + MaxConnections, + UseCheckAndSet, + element, + values, + attributesJson); + } +} diff --git a/src/StackExchange.Redis/VectorSetQuantization.cs b/src/StackExchange.Redis/VectorSetQuantization.cs index 0e5a3304f..d78f4b34b 100644 --- a/src/StackExchange.Redis/VectorSetQuantization.cs +++ b/src/StackExchange.Redis/VectorSetQuantization.cs @@ -11,20 +11,20 @@ public enum VectorSetQuantization /// /// Unknown or unrecognized quantization type. /// - Unknown, + Unknown = 0, /// /// No quantization (full precision). This maps to "NOQUANT" or "f32". /// - None, + None = 1, /// /// 8-bit integer quantization (default). This maps to "Q8" or "int8". /// - Int8, + Int8 = 2, /// /// Binary quantization. This maps to "BIN" or "bin". /// - Binary, + Binary = 3, } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs index f73041186..b4ff2091b 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -41,12 +41,12 @@ public void VectorSetAdd_BasicCall() { var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); - prefixed.VectorSetAdd("vectorset", "element1", vector); + var request = VectorSetAddRequest.Member("element1", vector); + prefixed.VectorSetAdd("vectorset", request); mock.Received().VectorSetAdd( "prefix:vectorset", - "element1", - vector); + request); } [Fact] @@ -55,28 +55,23 @@ public void VectorSetAdd_WithAllParameters() var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); var attributes = """{"category":"test"}"""; - prefixed.VectorSetAdd( - "vectorset", + var request = VectorSetAddRequest.Member( "element1", vector, - reducedDimensions: 64, - quantization: VectorSetQuantization.Binary, - buildExplorationFactor: 300, - maxConnections: 32, - useCheckAndSet: true, - attributesJson: attributes, + attributes); + request.ReducedDimensions = 64; + request.Quantization = VectorSetQuantization.Binary; + request.BuildExplorationFactor = 300; + request.MaxConnections = 32; + request.UseCheckAndSet = true; + prefixed.VectorSetAdd( + "vectorset", + request, flags: CommandFlags.FireAndForget); mock.Received().VectorSetAdd( "prefix:vectorset", - "element1", - vector, - 64, - VectorSetQuantization.Binary, - 300, - 32, - true, - attributes, + request, CommandFlags.FireAndForget); } diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index c71fa4359..12eda7147 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -27,7 +27,8 @@ public async Task VectorSetAdd_BasicOperation(bool suppressFp32) if (suppressFp32) VectorSetAddMessage.SuppressFp32(); try { - var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory(), null); + var result = await db.VectorSetAddAsync(key, request); Assert.True(result); } @@ -49,7 +50,8 @@ public async Task VectorSetAdd_WithAttributes() var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; var attributes = """{"category":"test","id":123}"""; - var result = await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), attributesJson: attributes); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory(), attributes); + var result = await db.VectorSetAddAsync(key, request); Assert.True(result); @@ -73,16 +75,18 @@ public async Task VectorSetAdd_WithEverything(VectorSetQuantization quantization var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; var attributes = """{"category":"test","id":123}"""; - var result = await db.VectorSetAddAsync( - key, + var request = VectorSetAddRequest.Member( "element1", vector.AsMemory(), - attributesJson: attributes, - useCheckAndSet: true, - quantization: quantization, - reducedDimensions: 64, - buildExplorationFactor: 300, - maxConnections: 32); + attributes); + request.Quantization = quantization; + request.ReducedDimensions = 64; + request.BuildExplorationFactor = 300; + request.MaxConnections = 32; + request.UseCheckAndSet = true; + var result = await db.VectorSetAddAsync( + key, + request); Assert.True(result); @@ -116,8 +120,10 @@ public async Task VectorSetLength_WithElements() var vector1 = new[] { 1.0f, 2.0f, 3.0f }; var vector2 = new[] { 4.0f, 5.0f, 6.0f }; - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); var length = await db.VectorSetLengthAsync(key); Assert.Equal(2, length); @@ -133,7 +139,8 @@ public async Task VectorSetDimension() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; - await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); var dimension = await db.VectorSetDimensionAsync(key); Assert.Equal(5, dimension); @@ -154,7 +161,8 @@ public async Task VectorSetContains(bool suppressFp32) if (suppressFp32) VectorSetAddMessage.SuppressFp32(); try { - await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); var exists = await db.VectorSetContainsAsync(key, "element1"); var notExists = await db.VectorSetContainsAsync(key, "element2"); @@ -183,7 +191,8 @@ public async Task VectorSetGetApproximateVector(bool suppressFp32) if (suppressFp32) VectorSetAddMessage.SuppressFp32(); try { - await db.VectorSetAddAsync(key, "element1", originalVector.AsMemory()); + var request = VectorSetAddRequest.Member("element1", originalVector.AsMemory()); + await db.VectorSetAddAsync(key, request); using var retrievedLease = await db.VectorSetGetApproximateVectorAsync(key, "element1"); @@ -215,7 +224,8 @@ public async Task VectorSetRemove() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new[] { 1.0f, 2.0f, 3.0f }; - await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); var removed = await db.VectorSetRemoveAsync(key, "element1"); Assert.True(removed); @@ -240,7 +250,9 @@ public async Task VectorSetInfo(VectorSetQuantization quantization) await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; - await db.VectorSetAddAsync(key, "element1", vector.AsMemory(), quantization: quantization); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + request.Quantization = quantization; + await db.VectorSetAddAsync(key, request); var info = await db.VectorSetInfoAsync(key); @@ -267,8 +279,10 @@ public async Task VectorSetRandomMember() var vector1 = new[] { 1.0f, 2.0f, 3.0f }; var vector2 = new[] { 4.0f, 5.0f, 6.0f }; - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); var randomMember = await db.VectorSetRandomMemberAsync(key); Assert.True(randomMember == "element1" || randomMember == "element2"); @@ -287,9 +301,12 @@ public async Task VectorSetRandomMembers() var vector2 = new[] { 4.0f, 5.0f, 6.0f }; var vector3 = new[] { 7.0f, 8.0f, 9.0f }; - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); - await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, request); var randomMembers = await db.VectorSetRandomMembersAsync(key, 2); @@ -317,9 +334,13 @@ public async Task VectorSetSimilaritySearch_ByVector(bool withScores, bool withA var vector2 = new[] { 0.0f, 1.0f, 0.0f }; var vector3 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); - await db.VectorSetAddAsync(key, "element3", vector3.AsMemory(), attributesJson: """{"category":"z"}"""); + var request = + VectorSetAddRequest.Member("element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory(), attributesJson: """{"category":"z"}"""); + await db.VectorSetAddAsync(key, request); // Search for vectors similar to vector1 var query = VectorSetSimilaritySearchRequest.ByVector(vector1.AsMemory()); @@ -369,8 +390,11 @@ public async Task VectorSetSimilaritySearch_ByMember(bool withScores, bool withA var vector1 = new[] { 1.0f, 0.0f, 0.0f }; var vector2 = new[] { 0.0f, 1.0f, 0.0f }; - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); + var request = + VectorSetAddRequest.Member("element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); + await db.VectorSetAddAsync(key, request); var query = VectorSetSimilaritySearchRequest.ByMember("element1"); query.Count = 1; @@ -434,7 +458,8 @@ void ScrambleVector() + JsonConvert.SerializeObject(new { id = i, region }) + (corruptSuffix ? "oops" : ""); ScrambleVector(); - await db.VectorSetAddAsync(key, $"element{i}", vector, attributesJson: json); + var request = VectorSetAddRequest.Member($"element{i}", vector.AsMemory(), json); + await db.VectorSetAddAsync(key, request); } ScrambleVector(); @@ -510,7 +535,8 @@ void ScrambleVector() var region = regions[rand.Next(regions.Length)]; var json = JsonConvert.SerializeObject(new { id = i, region }); ScrambleVector(); - await db.VectorSetAddAsync(key, $"element{i}", vector, attributesJson: json); + var request = VectorSetAddRequest.Member($"element{i}", vector.AsMemory(), json); + await db.VectorSetAddAsync(key, request); } ScrambleVector(); @@ -543,7 +569,8 @@ public async Task VectorSetSetAttributesJson() await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); var vector = new[] { 1.0f, 2.0f, 3.0f }; - await db.VectorSetAddAsync(key, "element1", vector.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); // Set attributes for existing element var attributes = """{"category":"updated","priority":"high","timestamp":"2024-01-01"}"""; @@ -574,9 +601,12 @@ public async Task VectorSetGetLinks() var vector2 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 var vector3 = new[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); - await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, request); // Get links for element1 (should include similar vectors) using var links = await db.VectorSetGetLinksAsync(key, "element1"); @@ -608,9 +638,12 @@ public async Task VectorSetGetLinksWithScores() var vector2 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 var vector3 = new[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 - await db.VectorSetAddAsync(key, "element1", vector1.AsMemory()); - await db.VectorSetAddAsync(key, "element2", vector2.AsMemory()); - await db.VectorSetAddAsync(key, "element3", vector3.AsMemory()); + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, request); // Get links with scores for element1 using var linksWithScores = await db.VectorSetGetLinksWithScoresAsync(key, "element1"); From 8004eb268c2e1e478153ec1747c4a709df46f001 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 10:16:32 +0100 Subject: [PATCH 30/32] unify attributesJson over jsonAttributes, and add [StringSyntax] appropriately --- .../Interfaces/IDatabase.VectorSets.cs | 7 +++++-- .../Interfaces/IDatabaseAsync.VectorSets.cs | 5 ++++- .../KeyspaceIsolation/KeyPrefixed.VectorSets.cs | 4 ++-- .../KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs | 4 ++-- src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt | 4 ++-- src/StackExchange.Redis/RedisDatabase.VectorSets.cs | 8 ++++---- .../VectorSetSimilaritySearchResult.cs | 3 +++ 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs index 7cad6e9b5..039075ec8 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -153,7 +153,7 @@ bool VectorSetAdd( /// /// The key of the vectorset. /// The member name. - /// The attributes to set as a JSON string. + /// The attributes to set as a JSON string. /// The flags to use for this operation. /// True if successful. /// @@ -161,7 +161,10 @@ bool VectorSetAdd( bool VectorSetSetAttributesJson( RedisKey key, RedisValue member, - string jsonAttributes, +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + string attributesJson, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs index 91f4b1863..863095140 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -80,7 +80,10 @@ Task VectorSetAddAsync( Task VectorSetSetAttributesJsonAsync( RedisKey key, RedisValue member, - string jsonAttributes, +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + string attributesJson, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs index 7d116c05f..809adad97 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -48,8 +48,8 @@ public Task VectorSetRandomMembersAsync(RedisKey key, long count, public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.VectorSetRemoveAsync(ToInner(key), member, flags); - public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, jsonAttributes, flags); + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, attributesJson, flags); public Task?> VectorSetSimilaritySearchAsync( RedisKey key, diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs index a5bedfdf9..62f4e9202 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs @@ -45,8 +45,8 @@ public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlag public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.VectorSetRemove(ToInner(key), member, flags); - public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) => - Inner.VectorSetSetAttributesJson(ToInner(key), member, jsonAttributes, flags); + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJson(ToInner(key), member, attributesJson, flags); public Lease? VectorSetSimilaritySearch( RedisKey key, diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 510525a03..47e9ce934 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -44,7 +44,7 @@ [SER001]StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue [SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! [SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -57,7 +57,7 @@ [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! jsonAttributes, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! [SER001]StackExchange.Redis.VectorSetInfo [SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs index 4340de3db..9b3f1b43b 100644 --- a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -81,9 +81,9 @@ public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags return ExecuteSync(msg, ResultProcessor.Boolean); } - public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson); return ExecuteSync(msg, ResultProcessor.Boolean); } @@ -173,9 +173,9 @@ public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandF return ExecuteAsync(msg, ResultProcessor.Boolean); } - public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string jsonAttributes, CommandFlags flags = CommandFlags.None) + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, jsonAttributes); + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson); return ExecuteAsync(msg, ResultProcessor.Boolean); } diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs index b86bd478f..fd912898b 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs @@ -22,6 +22,9 @@ public readonly struct VectorSetSimilaritySearchResult(RedisValue member, double /// /// The JSON attributes associated with the member when WITHATTRIBS is used, null otherwise. /// +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif public string? AttributesJson { get; } = attributesJson; /// From 167434be053ceba35d23c7a2cca5da06cb13a4d6 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 10:51:41 +0100 Subject: [PATCH 31/32] add more context on ConfigGet test failure --- tests/StackExchange.Redis.Tests/ConfigTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 995b66a5a..f9ca738ba 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -442,6 +442,16 @@ public async Task GetInfo() } var cpuCount = cpu.Count(); Assert.True(cpuCount > 2); + if (cpu.Key != "CPU") + { + // seem to be seeing this in logs; add lots of detail + var sb = new StringBuilder("Expected CPU, got ").AppendLine(cpu.Key); + foreach (var setting in cpu) + { + sb.Append(setting.Key).Append('=').AppendLine(setting.Value); + } + Assert.Fail(sb.ToString()); + } Assert.Equal("CPU", cpu.Key); Assert.Contains(cpu, x => x.Key == "used_cpu_sys"); Assert.Contains(cpu, x => x.Key == "used_cpu_user"); From 3f98c565fa01a98ef32e4f92099349fa85052dd6 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 11:05:21 +0100 Subject: [PATCH 32/32] move API to shipped --- .../PublicAPI/PublicAPI.Shipped.txt | 89 +++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 89 ------------------- 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index e82af2bee..10044dc9b 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1962,3 +1962,92 @@ StackExchange.Redis.ConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey StackExchange.Redis.IConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer! StackExchange.Redis.IServer.Execute(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! +[SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! +[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.VectorSetAddRequest +[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.get -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.get -> bool +[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.get -> double? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.get -> string? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.set -> void +[SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int +[SER001]StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinks(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinksWithScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.VectorSetInfo? +[SER001]StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +[SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksWithScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.VectorSetInfo +[SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int +[SER001]StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long +[SER001]StackExchange.Redis.VectorSetInfo.Length.get -> long +[SER001]StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int +[SER001]StackExchange.Redis.VectorSetInfo.Quantization.get -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetInfo.QuantizationRaw.get -> string? +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorSetQuantization quantization, string? quantizationRaw, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long +[SER001]StackExchange.Redis.VectorSetLink +[SER001]StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetLink.Score.get -> double +[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink() -> void +[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void +[SER001]StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Binary = 3 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Int8 = 2 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.None = 1 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Unknown = 0 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.AttributesJson.get -> string? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Score.get -> double +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult() -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void +[SER001]static StackExchange.Redis.VectorSetAddRequest.Member(StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, string? attributesJson = null) -> StackExchange.Redis.VectorSetAddRequest! +[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! +[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 47e9ce934..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,90 +1 @@ #nullable enable -[SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! -[SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! -[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.VectorSetAddRequest -[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.get -> int? -[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.set -> void -[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.get -> int? -[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.set -> void -[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.get -> StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.set -> void -[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.get -> int? -[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.set -> void -[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.get -> bool -[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.get -> int? -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.get -> bool -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.get -> double? -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.get -> string? -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.get -> int? -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.get -> int? -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.get -> bool -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.get -> bool -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.set -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.get -> bool -[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.set -> void -[SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int -[SER001]StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabase.VectorSetGetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? -[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinks(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinksWithScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.VectorSetInfo? -[SER001]StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! -[SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksWithScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.VectorSetInfo -[SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int -[SER001]StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long -[SER001]StackExchange.Redis.VectorSetInfo.Length.get -> long -[SER001]StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int -[SER001]StackExchange.Redis.VectorSetInfo.Quantization.get -> StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetInfo.QuantizationRaw.get -> string? -[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void -[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorSetQuantization quantization, string? quantizationRaw, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void -[SER001]StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long -[SER001]StackExchange.Redis.VectorSetLink -[SER001]StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue -[SER001]StackExchange.Redis.VectorSetLink.Score.get -> double -[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink() -> void -[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void -[SER001]StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetQuantization.Binary = 3 -> StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetQuantization.Int8 = 2 -> StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetQuantization.None = 1 -> StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetQuantization.Unknown = 0 -> StackExchange.Redis.VectorSetQuantization -[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult -[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.AttributesJson.get -> string? -[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Member.get -> StackExchange.Redis.RedisValue -[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Score.get -> double -[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult() -> void -[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void -[SER001]static StackExchange.Redis.VectorSetAddRequest.Member(StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, string? attributesJson = null) -> StackExchange.Redis.VectorSetAddRequest! -[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! -[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!