Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,83 @@ public void VectorIndexConfig_SkipDefaultQuantization_Deserializes_To_None_Quant
Assert.Equal("none", hnsw?.Quantizer.Type);
Assert.IsType<VectorIndex.Quantizers.None>(hnsw?.Quantizer);
}

/// <summary>
/// Tests that vector index config hfresh deserializes from json
/// </summary>
[Fact]
public void VectorIndexConfig_HFresh_From_Json()
{
var json =
@"{ ""distance"": ""cosine"", ""maxPostingSizeKB"": 256, ""replicas"": 4, ""searchProbe"": 64 }";

var config = JsonSerializer.Deserialize<Dictionary<string, object>>(
json,
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
);

var hfresh = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", config);

Assert.NotNull(hfresh);
Assert.Equal(VectorIndexConfig.VectorDistance.Cosine, hfresh?.Distance);
Assert.Equal(256, hfresh?.MaxPostingSizeKb);
Assert.Equal(4, hfresh?.Replicas);
Assert.Equal(64, hfresh?.SearchProbe);
Assert.Null(hfresh?.Quantizer);
Assert.Null(hfresh?.MultiVector);
}

/// <summary>
/// Tests that vector index config hfresh roundtrips through serialization
/// </summary>
[Fact]
public void VectorIndexConfig_HFresh_Roundtrip()
{
var original = new VectorIndex.HFresh
{
Distance = VectorIndexConfig.VectorDistance.Dot,
MaxPostingSizeKb = 512,
Replicas = 8,
SearchProbe = 128,
};

// Serialize to DTO → JSON → deserialize back
var json = VectorIndexSerialization.SerializeHFresh(original);
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
json,
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
);
var roundtripped = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", dict);

Assert.NotNull(roundtripped);
Assert.Equal(original.Distance, roundtripped?.Distance);
Assert.Equal(original.MaxPostingSizeKb, roundtripped?.MaxPostingSizeKb);
Assert.Equal(original.Replicas, roundtripped?.Replicas);
Assert.Equal(original.SearchProbe, roundtripped?.SearchProbe);
}

/// <summary>
/// Tests that vector index config hfresh preserves RQ quantizer through serialization
/// </summary>
[Fact]
public void VectorIndexConfig_HFresh_With_RQ_Quantizer()
{
var original = new VectorIndex.HFresh
{
Quantizer = new VectorIndex.Quantizers.RQ { Bits = 8, RescoreLimit = 20 },
};

var json = VectorIndexSerialization.SerializeHFresh(original);
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
json,
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
);
var roundtripped = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", dict);

Assert.NotNull(roundtripped?.Quantizer);
var rq = Assert.IsType<VectorIndex.Quantizers.RQ>(roundtripped?.Quantizer);
Assert.Equal("rq", rq.Type);
Assert.Equal(8, rq.Bits);
Assert.Equal(20, rq.RescoreLimit);
}
}
144 changes: 144 additions & 0 deletions src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,50 @@ internal class DynamicDto
public FlatDto? Flat { get; set; }
}

/// <summary>
/// The hfresh dto class
/// </summary>
internal class HFreshDto
{
/// <summary>
/// Gets or sets the value of the distance
/// </summary>
[JsonPropertyName("distance")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public VectorDistance? Distance { get; set; }

/// <summary>
/// Gets or sets the maximum posting list size in KB.
/// Note: JSON key uses uppercase "KB" per Weaviate API convention.
/// </summary>
[JsonPropertyName("maxPostingSizeKB")]
public int? MaxPostingSizeKb { get; set; }

/// <summary>
/// Gets or sets the value of the replicas
/// </summary>
[JsonPropertyName("replicas")]
public int? Replicas { get; set; }

/// <summary>
/// Gets or sets the value of the search probe
/// </summary>
[JsonPropertyName("searchProbe")]
public int? SearchProbe { get; set; }

/// <summary>
/// Gets or sets the RQ quantizer. Only RQ is supported for HFresh.
/// </summary>
[JsonPropertyName("rq")]
public VectorIndex.Quantizers.RQ? RQ { get; set; }

/// <summary>
/// Gets or sets the value of the multi vector
/// </summary>
[JsonPropertyName("multivector")]
public MultiVectorDto? MultiVector { get; set; }
}

// Extension methods for mapping
/// <summary>
/// The vector index mapping extensions class
Expand Down Expand Up @@ -526,6 +570,79 @@ public static DynamicDto ToDto(this VectorIndex.Dynamic dynamic)
Flat = dynamic.Flat?.ToDto(),
};
}

// HFresh mapping
/// <summary>
/// Returns the hfresh using the specified dto
/// </summary>
/// <param name="dto">The dto</param>
/// <returns>The vector index hfresh</returns>
public static VectorIndex.HFresh ToHFresh(this HFreshDto dto)
{
var muvera = dto.MultiVector?.Muvera?.ToModel();
var multivector =
dto.MultiVector != null && dto.MultiVector.Enabled == true
? new MultiVectorConfig
{
Aggregation = dto.MultiVector.Aggregation,
Encoding = muvera,
}
: null;

return new VectorIndex.HFresh
{
Distance = dto.Distance,
MaxPostingSizeKb = dto.MaxPostingSizeKb,
Replicas = dto.Replicas,
SearchProbe = dto.SearchProbe,
Quantizer = dto.RQ?.Enabled == true ? dto.RQ : null,
MultiVector = multivector,
};
}

/// <summary>
/// Returns the dto using the specified hfresh
/// </summary>
/// <param name="hfresh">The hfresh</param>
/// <returns>The hfresh dto</returns>
public static HFreshDto ToDto(this VectorIndex.HFresh hfresh)
{
return new HFreshDto
{
Distance = hfresh.Distance,
MaxPostingSizeKb = hfresh.MaxPostingSizeKb,
Replicas = hfresh.Replicas,
SearchProbe = hfresh.SearchProbe,
RQ = hfresh.Quantizer switch
{
VectorIndex.Quantizers.RQ rq => rq,
null => null,
_ => throw new WeaviateClientException(
$"HFresh only supports RQ quantization, but got '{hfresh.Quantizer.Type}'."
),
},
MultiVector =
hfresh.MultiVector != null
? new MultiVectorDto
{
Enabled = true,
Muvera = (hfresh.MultiVector.Encoding as MuveraEncoding)?.ToDto(),
Aggregation = hfresh.MultiVector.Aggregation,
}
: new MultiVectorDto
{
Enabled = false,
Aggregation = "maxSim",
Muvera = new MuveraDto
{
Enabled = false,
KSim = 4,
DProjections = 16,
Repetitions = 10,
},
},
};
}
}

/// <summary>
Expand All @@ -551,6 +668,7 @@ internal static class VectorIndexSerialization
VectorIndex.HNSW.TypeValue => (VectorIndexConfig?)DeserializeHnsw(vic),
VectorIndex.Flat.TypeValue => DeserializeFlat(vic),
VectorIndex.Dynamic.TypeValue => DeserializeDynamic(vic),
VectorIndex.HFresh.TypeValue => DeserializeHFresh(vic),
_ => null,
};

Expand All @@ -571,6 +689,7 @@ internal static class VectorIndexSerialization
VectorIndex.HNSW hnsw => (object?)hnsw.ToDto(),
VectorIndex.Flat flat => (object?)flat.ToDto(),
VectorIndex.Dynamic dynamic => (object?)dynamic.ToDto(),
VectorIndex.HFresh hfresh => (object?)hfresh.ToDto(),
_ => null,
};

Expand Down Expand Up @@ -648,4 +767,29 @@ public static VectorIndex.Dynamic DeserializeDynamic(IDictionary<string, object?
);
return dto?.ToDynamic() ?? new VectorIndex.Dynamic() { Flat = null, Hnsw = null };
}

/// <summary>
/// Serializes the hfresh using the specified hfresh
/// </summary>
/// <param name="hfresh">The hfresh</param>
/// <returns>The string</returns>
public static string SerializeHFresh(VectorIndex.HFresh hfresh)
{
var dto = hfresh.ToDto();
return JsonSerializer.Serialize(dto, Rest.WeaviateRestClient.RestJsonSerializerOptions);
}

/// <summary>
/// Deserializes the hfresh using the specified json
/// </summary>
/// <param name="json">The json</param>
/// <returns>The vector index hfresh</returns>
public static VectorIndex.HFresh DeserializeHFresh(IDictionary<string, object?> json)
{
var dto = JsonSerializer.Deserialize<HFreshDto>(
JsonSerializer.Serialize(json, Rest.WeaviateRestClient.RestJsonSerializerOptions),
Rest.WeaviateRestClient.RestJsonSerializerOptions
);
return dto?.ToHFresh() ?? new VectorIndex.HFresh();
}
}
44 changes: 44 additions & 0 deletions src/Weaviate.Client/Models/VectorIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,48 @@ public sealed record Dynamic : VectorIndexConfig
[JsonIgnore]
public override string Type => TypeValue;
}

/// <summary>
/// Configuration for HFresh (inverted-list ANN) vector index.
/// Requires Weaviate 1.36 or later.
/// </summary>
public sealed record HFresh : VectorIndexConfig
{
/// <summary>The type discriminator string used by the Weaviate REST API.</summary>
public const string TypeValue = "hfresh";

/// <summary>Gets or sets the distance metric for vector similarity.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public VectorDistance? Distance { get; set; }

/// <summary>
/// Gets or sets the maximum posting list size in KB.
/// When null, Weaviate computes a value based on the dataset size.
/// </summary>
public int? MaxPostingSizeKb { get; set; }

/// <summary>
/// Gets or sets the number of posting lists across which vectors are distributed.
/// Server default: 4.
/// </summary>
public int? Replicas { get; set; }

/// <summary>
/// Gets or sets the number of posting lists probed at query time.
/// Higher values improve recall at the cost of throughput. Server default: 64.
/// </summary>
public int? SearchProbe { get; set; }

/// <summary>
/// Gets or sets the quantizer configuration. Only RQ is supported for HFresh.
/// </summary>
public QuantizerConfigBase? Quantizer { get; set; }

/// <summary>Gets or sets the multi-vector configuration.</summary>
public MultiVectorConfig? MultiVector { get; set; }

/// <inheritdoc/>
[JsonIgnore]
public override string Type => TypeValue;
}
}
24 changes: 24 additions & 0 deletions src/Weaviate.Client/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const Weaviate.Client.Models.Reranker.VoyageAI.Models.RerankLite1 = "rerank-lite
const Weaviate.Client.Models.Reranker.VoyageAI.TypeValue = "reranker-voyageai" -> string!
const Weaviate.Client.Models.VectorIndex.Dynamic.TypeValue = "dynamic" -> string!
const Weaviate.Client.Models.VectorIndex.Flat.TypeValue = "flat" -> string!
const Weaviate.Client.Models.VectorIndex.HFresh.TypeValue = "hfresh" -> string!
const Weaviate.Client.Models.VectorIndex.HNSW.TypeValue = "hnsw" -> string!
const Weaviate.Client.Models.VectorIndex.Quantizers.BQ.TypeValue = "bq" -> string!
const Weaviate.Client.Models.VectorIndex.Quantizers.None.TypeValue = "none" -> string!
Expand Down Expand Up @@ -146,6 +147,7 @@ override sealed Weaviate.Client.Models.TypedGuid.Equals(Weaviate.Client.Models.T
override sealed Weaviate.Client.Models.TypedValue<T>.Equals(Weaviate.Client.Models.TypedBase<T>? other) -> bool
override sealed Weaviate.Client.Models.VectorIndex.Dynamic.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
override sealed Weaviate.Client.Models.VectorIndex.Flat.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
override sealed Weaviate.Client.Models.VectorIndex.HFresh.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
override sealed Weaviate.Client.Models.VectorIndex.HNSW.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
override sealed Weaviate.Client.Models.VectorIndex.Quantizers.BQ.Equals(Weaviate.Client.Models.VectorIndexConfig.QuantizerConfigFlat? other) -> bool
override sealed Weaviate.Client.Models.VectorIndex.Quantizers.None.Equals(Weaviate.Client.Models.VectorIndexConfig.QuantizerConfigBase? other) -> bool
Expand Down Expand Up @@ -960,6 +962,11 @@ override Weaviate.Client.Models.VectorIndex.Flat.Equals(object? obj) -> bool
override Weaviate.Client.Models.VectorIndex.Flat.GetHashCode() -> int
override Weaviate.Client.Models.VectorIndex.Flat.ToString() -> string!
override Weaviate.Client.Models.VectorIndex.Flat.Type.get -> string!
override Weaviate.Client.Models.VectorIndex.HFresh.<Clone>$() -> Weaviate.Client.Models.VectorIndex.HFresh!
override Weaviate.Client.Models.VectorIndex.HFresh.Equals(object? obj) -> bool
override Weaviate.Client.Models.VectorIndex.HFresh.GetHashCode() -> int
override Weaviate.Client.Models.VectorIndex.HFresh.ToString() -> string!
override Weaviate.Client.Models.VectorIndex.HFresh.Type.get -> string!
override Weaviate.Client.Models.VectorIndex.HNSW.<Clone>$() -> Weaviate.Client.Models.VectorIndex.HNSW!
override Weaviate.Client.Models.VectorIndex.HNSW.Equals(object? obj) -> bool
override Weaviate.Client.Models.VectorIndex.HNSW.GetHashCode() -> int
Expand Down Expand Up @@ -1814,6 +1821,8 @@ static Weaviate.Client.Models.VectorIndex.Dynamic.operator !=(Weaviate.Client.Mo
static Weaviate.Client.Models.VectorIndex.Dynamic.operator ==(Weaviate.Client.Models.VectorIndex.Dynamic? left, Weaviate.Client.Models.VectorIndex.Dynamic? right) -> bool
static Weaviate.Client.Models.VectorIndex.Flat.operator !=(Weaviate.Client.Models.VectorIndex.Flat? left, Weaviate.Client.Models.VectorIndex.Flat? right) -> bool
static Weaviate.Client.Models.VectorIndex.Flat.operator ==(Weaviate.Client.Models.VectorIndex.Flat? left, Weaviate.Client.Models.VectorIndex.Flat? right) -> bool
static Weaviate.Client.Models.VectorIndex.HFresh.operator !=(Weaviate.Client.Models.VectorIndex.HFresh? left, Weaviate.Client.Models.VectorIndex.HFresh? right) -> bool
static Weaviate.Client.Models.VectorIndex.HFresh.operator ==(Weaviate.Client.Models.VectorIndex.HFresh? left, Weaviate.Client.Models.VectorIndex.HFresh? right) -> bool
static Weaviate.Client.Models.VectorIndex.HNSW.operator !=(Weaviate.Client.Models.VectorIndex.HNSW? left, Weaviate.Client.Models.VectorIndex.HNSW? right) -> bool
static Weaviate.Client.Models.VectorIndex.HNSW.operator ==(Weaviate.Client.Models.VectorIndex.HNSW? left, Weaviate.Client.Models.VectorIndex.HNSW? right) -> bool
static Weaviate.Client.Models.VectorIndex.Quantizers.BQ.operator !=(Weaviate.Client.Models.VectorIndex.Quantizers.BQ? left, Weaviate.Client.Models.VectorIndex.Quantizers.BQ? right) -> bool
Expand Down Expand Up @@ -5450,6 +5459,21 @@ Weaviate.Client.Models.VectorIndex.Flat.Quantizer.get -> Weaviate.Client.Models.
Weaviate.Client.Models.VectorIndex.Flat.Quantizer.set -> void
Weaviate.Client.Models.VectorIndex.Flat.VectorCacheMaxObjects.get -> long?
Weaviate.Client.Models.VectorIndex.Flat.VectorCacheMaxObjects.set -> void
Weaviate.Client.Models.VectorIndex.HFresh
Weaviate.Client.Models.VectorIndex.HFresh.Distance.get -> Weaviate.Client.Models.VectorIndexConfig.VectorDistance?
Weaviate.Client.Models.VectorIndex.HFresh.Distance.set -> void
Weaviate.Client.Models.VectorIndex.HFresh.Equals(Weaviate.Client.Models.VectorIndex.HFresh? other) -> bool
Weaviate.Client.Models.VectorIndex.HFresh.HFresh() -> void
Weaviate.Client.Models.VectorIndex.HFresh.MaxPostingSizeKb.get -> int?
Weaviate.Client.Models.VectorIndex.HFresh.MaxPostingSizeKb.set -> void
Weaviate.Client.Models.VectorIndex.HFresh.MultiVector.get -> Weaviate.Client.Models.VectorIndexConfig.MultiVectorConfig?
Weaviate.Client.Models.VectorIndex.HFresh.MultiVector.set -> void
Weaviate.Client.Models.VectorIndex.HFresh.Quantizer.get -> Weaviate.Client.Models.VectorIndexConfig.QuantizerConfigBase?
Weaviate.Client.Models.VectorIndex.HFresh.Quantizer.set -> void
Weaviate.Client.Models.VectorIndex.HFresh.Replicas.get -> int?
Weaviate.Client.Models.VectorIndex.HFresh.Replicas.set -> void
Weaviate.Client.Models.VectorIndex.HFresh.SearchProbe.get -> int?
Weaviate.Client.Models.VectorIndex.HFresh.SearchProbe.set -> void
Weaviate.Client.Models.VectorIndex.HNSW
Weaviate.Client.Models.VectorIndex.HNSW.CleanupIntervalSeconds.get -> int?
Weaviate.Client.Models.VectorIndex.HNSW.CleanupIntervalSeconds.set -> void
Expand Down
Loading