Skip to content

Conversation

mgravell
Copy link
Collaborator

@mgravell mgravell commented Aug 11, 2025

as per https://redis.io/docs/latest/develop/data-types/vector-sets/

  • all methods/types start VectorSet...
  • all core methods implemented
  • the only "unusual" one is VLINKS; the server returns this as nested data, but the nesting is not meaningful to the caller (instead being related to the server core), so I've flattened the result
  • usage is shown in VectorSetIntegrationTests, in particular VectorSetSimilaritySearch_WithFilter is useful for overview - since the main two primary APIs are VADD and VSIM
  • since vector set data can be non-trivial, all return types lean on Lease<T> rather than T[]. Inputs are ReadOnlyMemory<T>; the exception to this is string data for JSON and filters; these are explicitly text, never blobs, so RedisValue seems inappropriate. I think forcing string is OK here
  • in acknowledgement that some of our files are too large, I've started splitting the VectorSet* bits out via partial files; I will follow this up with "everything else" (.Strings.cs, .Hashes.cs) in a separate PR

This PR also introduces the start of a new literal-matching API, ala FastHash. It is intended that this will be extended at a later date.

CI: may be dependent on the vectorset module; if it fails, I'll add suitable validation. All VectorSetTests pass locally:

image

@mgravell mgravell marked this pull request as draft August 11, 2025 13:51
@mgravell mgravell marked this pull request as ready for review August 13, 2025 15:57
@mgravell
Copy link
Collaborator Author

mgravell commented Aug 14, 2025

Additional thoughts on FastHash:

  • if length <= 8, we can skip equality test
  • consider hashing first 16 (status: considered and deferred)
  • add unit test that shows interesting known values of different lengths (and different values at same length)

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 4, 2025

nits and "avoid the massive number of parameters" type decl: d53f1ab

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 4, 2025

image

@mgravell mgravell requested a review from NickCraver September 4, 2025 16:03
@badrishc
Copy link

badrishc commented Sep 4, 2025

Brilliant, glad to see this landing as we look into vector sets in Garnet as well.

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 5, 2025

@NickCraver @philon-msft bump

@kevin-montrose
Copy link
Contributor

Brilliant, glad to see this landing as we look into vector sets in Garnet as well.

In this vein, as I'm working on Garnet's Vector Set api support, I want to have a discussion about likely extensions.

It seems quite possible that Redis will add new quantization options (to join NOQUANT, B8, and BIN). Garnet is definitely going to have a few (I've sketched out a XPREQ8, though that name is a place holder, for "NOQUANT but unsigned bytes"), and I expect more to come.

It also seems reasonable for there to be new vector data formats for the commands (to join FP32 and VALUES), given how common reduced precision storage is in ML workloads (FP16 and FP8 for example). Garnet is going to have at least one (which I'm calling XB8 right now, for "each dimension is one byte, [0, 255]").

Obviously all these extensions could be handled with the .ExecuteXXX methods just fine. But I wonder, given the likelihood of extensions from both Redis proper and other RESP implementers, if a something for these specific options might be merited.

Just spit-balling, but a struct (which wraps some Span<byte>/Memory<byte>) instead of an enum for VectorSetQuantization and a new type for values rather than a simple ReadOnlyMemory<float> with an accompanying Span<byte>/Memory<byte> for the flag. With some conversions, this could also accommodate users who have their vectors elements as strings w/o forcing them to parse them client side before sending.

/cc @NickCraver Since we discuss this a little bit this morning.

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 5, 2025

Sounds interesting. We've moved to an args parameter for that API, so one option here might be to, at some future date, unseal it with a view to using some polymorphic "the subclass can fill this buffer with what it wants" API. I think this gives us any wriggle room for future changes. Thoughts?

In particular, I wonder if I move the existing "by vector" bits into a subclass, presumably with a suitable "fill this" API.

@kevin-montrose
Copy link
Contributor

Lemme see if I follow.

Current API for VADD is:

bool VectorSetAdd(
        RedisKey key,
        RedisValue element,
        ReadOnlyMemory<float> values,
        int? reducedDimensions = null,
        VectorSetQuantization quantization = VectorSetQuantization.Int8,
        int? buildExplorationFactor = null,
        int? maxConnections = null,
        bool useCheckAndSet = false,
        string? attributesJson = null,
        CommandFlags flags = CommandFlags.None);

public enum VectorSetQuantization
{
    Unknown,
    None,
    Int8,
    Binary,
}

So we'd change (or add as a new overload) that to something like:

bool VectorSetAdd<TVectorData>(
        RedisKey key,
        RedisValue element,
        TVectorData values,
        int? reducedDimensions = null,
        VectorSetQuantization quantization = VectorSetQuantization.Int8,
        int? buildExplorationFactor = null,
        int? maxConnections = null,
        bool useCheckAndSet = false,
        string? attributesJson = null,
        CommandFlags flags = CommandFlags.None)
where TVectorData: IVectorData;

public readonly struct VectorSetQuantization(ReadOnlyMemory<byte> Name)
{
  public static readonly VectorSetQuantization Unknown = new(default);
  public static readonly VectorSetQuantization None = new("NOQUANT"u8);
  public static readonly VectorSetQuantization Int8 = new("Q8"u8);
  public static readonly VectorSetQuantization Binary = new("BIN"u8);
}

public interface IVectorData
{
  // Some sort of write callback here?
}

That makes sense to me.

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 6, 2025

Sorry, I was only thinking about VSIM - my bad. It would seem desirable to unify the approach between VSIM and VADD, and maybe marry the data and quant encoding (understanding that there is some overlap). I wonder if we should take a leaf from System.Text.Encoding here. Suggestion:

// Interprets a single T as N elements. T is most likely
// ROM-float, ROM-double, or similar; but is determined
// by the implementation
public abstract class VectorEncoding<TVector>
{
    public abstract ReadOnlySpan<byte> DataEncoding {get;} // "FP32"u8
    public abstract ReadOnlySpan<byte> Quantization {get;} // "INT8"u8
    // note: if -ve reply from TryGetByteCount, VALUES is used instead
    public abstract int TryGetByteCount(in TVector vector); // 4x.Length
    public abstract void GetBytes(in TVector vector, Span<byte> buffer); // Unsafe.Cast ... CopyTo

    // used for VALUES encoding
    public abstract int GetElementCount(in TVector vector); // .Length
    public abstract double GetElement(in TVector vector, int index); // [index]
}

Where both VSIM and VADD could take a TVector and a VectorEncoding-TVector

Thoughts?

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 6, 2025

Or are the quantization and encoding entirely independent? They don't feel entirely independent....

@kevin-montrose
Copy link
Contributor

Or are the quantization and encoding entirely independent? They don't feel entirely independent....

They are, though the feel is weird I agree.

But quant (BIN|Q8|NOQUANT|...) is separate since it defines storage in the vector set, and data (VALUES|FP32|...) defines storage in the command only.

Like, NOQUANT + XB8 still stores floats in the vector set in this scheme, while FP32 + XPREQ stores bytes. The data encoding is transient.

Some of this confusion is because NOQUANT doesn't mention F32, even though that is what is actually means.

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 7, 2025

OK. Right. I'll see if I can keep the two concepts separate, then. But there's definitely facility in abstraction - no point sending FP32 queries or data-loads to data that is quantized to FP8, so I can easily imagine that you'd allow an efficient transport syntax

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 9, 2025

@kevin-montrose I looked at this this morning. I think there's a very strong likelihood of over-designing if we don't have concrete requirements, and in any event: we'd probably still want the default and most obvious API / overload to be the existing one. I think, for now, we can do a minimal change here that keeps the door open:

  • make VectorSetSimilaritySearchRequest abstract
  • create two internal versions - one for "by member", one for "by vector, FP32"
  • create two static methods on VSSSR

so:

- [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Member.get -> StackExchange.Redis.RedisValue
- [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Member.set -> void
- [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Vector.get -> System.ReadOnlyMemory<float>
- [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Vector.set -> void
- [SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.VectorSetSimilaritySearchRequest() -> void
+ [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!
+ [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory<float> vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!

This means that in the future, we can add whatever other factory methods we need to support additional encodings, and we haven't forced VectorSetSimilaritySearchRequest.Vector to be a fixed type.

On the "add" side: we can just overload trivially.

I'm adding this as a commit - feedback encouraged before I merge ;p

(the actual API to allow extensibility is internal for now - that's fine; all are options are open)

 - make VectorSetSimilaritySearchRequest abstract
 - remove Member and Vector
 - add ByMember and ByVector factory methods
 - (internal changes to support the above, in the message etc)
@mgravell
Copy link
Collaborator Author

mgravell commented Sep 9, 2025

API change formalized ^^^

@kevin-montrose
Copy link
Contributor

I'm fine being conservative until we have the final Garnet (or others) implementation to compare against (and obviously, consult about), and that seems reasonable.

Though this still leaves the question of VADD extensions unaddressed, yes?

/// <summary>
/// Binary quantization. This maps to "BIN" or "bin".
/// </summary>
Binary,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in case, let's add int values to this

Copy link
Collaborator

@NickCraver NickCraver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed doing an object for VectorSetAdd for future additions, otherwise looking good 👍

@mgravell
Copy link
Collaborator Author

mgravell commented Sep 9, 2025

@kevin-montrose see Nick's comment above: working exactly that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants