Skip to content

Commit 2e63a73

Browse files
mpartipiloclaude
andcommitted
feat: add HFresh vector index type support (#285)
Add VectorIndex.HFresh as a first-class vector index configuration type, matching feature parity with the Python client (PR weaviate-python-client#1848). HFresh is an inverted-list-based ANN index introduced in Weaviate 1.36. Vectors are distributed across posting lists (Replicas) and queries probe SearchProbe lists for candidates. Supports RQ quantization and multi-vector configurations. MaxPostingSizeKb can be left unset for server-computed sizing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 202f7b1 commit 2e63a73

4 files changed

Lines changed: 291 additions & 0 deletions

File tree

src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,83 @@ public void VectorIndexConfig_SkipDefaultQuantization_Deserializes_To_None_Quant
120120
Assert.Equal("none", hnsw?.Quantizer.Type);
121121
Assert.IsType<VectorIndex.Quantizers.None>(hnsw?.Quantizer);
122122
}
123+
124+
/// <summary>
125+
/// Tests that vector index config hfresh deserializes from json
126+
/// </summary>
127+
[Fact]
128+
public void VectorIndexConfig_HFresh_From_Json()
129+
{
130+
var json =
131+
@"{ ""distance"": ""cosine"", ""maxPostingSizeKB"": 256, ""replicas"": 4, ""searchProbe"": 64 }";
132+
133+
var config = JsonSerializer.Deserialize<Dictionary<string, object>>(
134+
json,
135+
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
136+
);
137+
138+
var hfresh = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", config);
139+
140+
Assert.NotNull(hfresh);
141+
Assert.Equal(VectorIndexConfig.VectorDistance.Cosine, hfresh?.Distance);
142+
Assert.Equal(256, hfresh?.MaxPostingSizeKb);
143+
Assert.Equal(4, hfresh?.Replicas);
144+
Assert.Equal(64, hfresh?.SearchProbe);
145+
Assert.Null(hfresh?.Quantizer);
146+
Assert.Null(hfresh?.MultiVector);
147+
}
148+
149+
/// <summary>
150+
/// Tests that vector index config hfresh roundtrips through serialization
151+
/// </summary>
152+
[Fact]
153+
public void VectorIndexConfig_HFresh_Roundtrip()
154+
{
155+
var original = new VectorIndex.HFresh
156+
{
157+
Distance = VectorIndexConfig.VectorDistance.Dot,
158+
MaxPostingSizeKb = 512,
159+
Replicas = 8,
160+
SearchProbe = 128,
161+
};
162+
163+
// Serialize to DTO → JSON → deserialize back
164+
var json = VectorIndexSerialization.SerializeHFresh(original);
165+
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
166+
json,
167+
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
168+
);
169+
var roundtripped = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", dict);
170+
171+
Assert.NotNull(roundtripped);
172+
Assert.Equal(original.Distance, roundtripped?.Distance);
173+
Assert.Equal(original.MaxPostingSizeKb, roundtripped?.MaxPostingSizeKb);
174+
Assert.Equal(original.Replicas, roundtripped?.Replicas);
175+
Assert.Equal(original.SearchProbe, roundtripped?.SearchProbe);
176+
}
177+
178+
/// <summary>
179+
/// Tests that vector index config hfresh preserves RQ quantizer through serialization
180+
/// </summary>
181+
[Fact]
182+
public void VectorIndexConfig_HFresh_With_RQ_Quantizer()
183+
{
184+
var original = new VectorIndex.HFresh
185+
{
186+
Quantizer = new VectorIndex.Quantizers.RQ { Bits = 8, RescoreLimit = 20 },
187+
};
188+
189+
var json = VectorIndexSerialization.SerializeHFresh(original);
190+
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
191+
json,
192+
Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions
193+
);
194+
var roundtripped = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", dict);
195+
196+
Assert.NotNull(roundtripped?.Quantizer);
197+
var rq = Assert.IsType<VectorIndex.Quantizers.RQ>(roundtripped?.Quantizer);
198+
Assert.Equal("rq", rq.Type);
199+
Assert.Equal(8, rq.Bits);
200+
Assert.Equal(20, rq.RescoreLimit);
201+
}
123202
}

src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,50 @@ internal class DynamicDto
273273
public FlatDto? Flat { get; set; }
274274
}
275275

276+
/// <summary>
277+
/// The hfresh dto class
278+
/// </summary>
279+
internal class HFreshDto
280+
{
281+
/// <summary>
282+
/// Gets or sets the value of the distance
283+
/// </summary>
284+
[JsonPropertyName("distance")]
285+
[JsonConverter(typeof(JsonStringEnumConverter))]
286+
public VectorDistance? Distance { get; set; }
287+
288+
/// <summary>
289+
/// Gets or sets the maximum posting list size in KB.
290+
/// Note: JSON key uses uppercase "KB" per Weaviate API convention.
291+
/// </summary>
292+
[JsonPropertyName("maxPostingSizeKB")]
293+
public int? MaxPostingSizeKb { get; set; }
294+
295+
/// <summary>
296+
/// Gets or sets the value of the replicas
297+
/// </summary>
298+
[JsonPropertyName("replicas")]
299+
public int? Replicas { get; set; }
300+
301+
/// <summary>
302+
/// Gets or sets the value of the search probe
303+
/// </summary>
304+
[JsonPropertyName("searchProbe")]
305+
public int? SearchProbe { get; set; }
306+
307+
/// <summary>
308+
/// Gets or sets the RQ quantizer. Only RQ is supported for HFresh.
309+
/// </summary>
310+
[JsonPropertyName("rq")]
311+
public VectorIndex.Quantizers.RQ? RQ { get; set; }
312+
313+
/// <summary>
314+
/// Gets or sets the value of the multi vector
315+
/// </summary>
316+
[JsonPropertyName("multivector")]
317+
public MultiVectorDto? MultiVector { get; set; }
318+
}
319+
276320
// Extension methods for mapping
277321
/// <summary>
278322
/// The vector index mapping extensions class
@@ -526,6 +570,79 @@ public static DynamicDto ToDto(this VectorIndex.Dynamic dynamic)
526570
Flat = dynamic.Flat?.ToDto(),
527571
};
528572
}
573+
574+
// HFresh mapping
575+
/// <summary>
576+
/// Returns the hfresh using the specified dto
577+
/// </summary>
578+
/// <param name="dto">The dto</param>
579+
/// <returns>The vector index hfresh</returns>
580+
public static VectorIndex.HFresh ToHFresh(this HFreshDto dto)
581+
{
582+
var muvera = dto.MultiVector?.Muvera?.ToModel();
583+
var multivector =
584+
dto.MultiVector != null && dto.MultiVector.Enabled == true
585+
? new MultiVectorConfig
586+
{
587+
Aggregation = dto.MultiVector.Aggregation,
588+
Encoding = muvera,
589+
}
590+
: null;
591+
592+
return new VectorIndex.HFresh
593+
{
594+
Distance = dto.Distance,
595+
MaxPostingSizeKb = dto.MaxPostingSizeKb,
596+
Replicas = dto.Replicas,
597+
SearchProbe = dto.SearchProbe,
598+
Quantizer = dto.RQ?.Enabled == true ? dto.RQ : null,
599+
MultiVector = multivector,
600+
};
601+
}
602+
603+
/// <summary>
604+
/// Returns the dto using the specified hfresh
605+
/// </summary>
606+
/// <param name="hfresh">The hfresh</param>
607+
/// <returns>The hfresh dto</returns>
608+
public static HFreshDto ToDto(this VectorIndex.HFresh hfresh)
609+
{
610+
return new HFreshDto
611+
{
612+
Distance = hfresh.Distance,
613+
MaxPostingSizeKb = hfresh.MaxPostingSizeKb,
614+
Replicas = hfresh.Replicas,
615+
SearchProbe = hfresh.SearchProbe,
616+
RQ = hfresh.Quantizer switch
617+
{
618+
VectorIndex.Quantizers.RQ rq => rq,
619+
null => null,
620+
_ => throw new WeaviateClientException(
621+
$"HFresh only supports RQ quantization, but got '{hfresh.Quantizer.Type}'."
622+
),
623+
},
624+
MultiVector =
625+
hfresh.MultiVector != null
626+
? new MultiVectorDto
627+
{
628+
Enabled = true,
629+
Muvera = (hfresh.MultiVector.Encoding as MuveraEncoding)?.ToDto(),
630+
Aggregation = hfresh.MultiVector.Aggregation,
631+
}
632+
: new MultiVectorDto
633+
{
634+
Enabled = false,
635+
Aggregation = "maxSim",
636+
Muvera = new MuveraDto
637+
{
638+
Enabled = false,
639+
KSim = 4,
640+
DProjections = 16,
641+
Repetitions = 10,
642+
},
643+
},
644+
};
645+
}
529646
}
530647

531648
/// <summary>
@@ -551,6 +668,7 @@ internal static class VectorIndexSerialization
551668
VectorIndex.HNSW.TypeValue => (VectorIndexConfig?)DeserializeHnsw(vic),
552669
VectorIndex.Flat.TypeValue => DeserializeFlat(vic),
553670
VectorIndex.Dynamic.TypeValue => DeserializeDynamic(vic),
671+
VectorIndex.HFresh.TypeValue => DeserializeHFresh(vic),
554672
_ => null,
555673
};
556674

@@ -571,6 +689,7 @@ internal static class VectorIndexSerialization
571689
VectorIndex.HNSW hnsw => (object?)hnsw.ToDto(),
572690
VectorIndex.Flat flat => (object?)flat.ToDto(),
573691
VectorIndex.Dynamic dynamic => (object?)dynamic.ToDto(),
692+
VectorIndex.HFresh hfresh => (object?)hfresh.ToDto(),
574693
_ => null,
575694
};
576695

@@ -648,4 +767,29 @@ public static VectorIndex.Dynamic DeserializeDynamic(IDictionary<string, object?
648767
);
649768
return dto?.ToDynamic() ?? new VectorIndex.Dynamic() { Flat = null, Hnsw = null };
650769
}
770+
771+
/// <summary>
772+
/// Serializes the hfresh using the specified hfresh
773+
/// </summary>
774+
/// <param name="hfresh">The hfresh</param>
775+
/// <returns>The string</returns>
776+
public static string SerializeHFresh(VectorIndex.HFresh hfresh)
777+
{
778+
var dto = hfresh.ToDto();
779+
return JsonSerializer.Serialize(dto, Rest.WeaviateRestClient.RestJsonSerializerOptions);
780+
}
781+
782+
/// <summary>
783+
/// Deserializes the hfresh using the specified json
784+
/// </summary>
785+
/// <param name="json">The json</param>
786+
/// <returns>The vector index hfresh</returns>
787+
public static VectorIndex.HFresh DeserializeHFresh(IDictionary<string, object?> json)
788+
{
789+
var dto = JsonSerializer.Deserialize<HFreshDto>(
790+
JsonSerializer.Serialize(json, Rest.WeaviateRestClient.RestJsonSerializerOptions),
791+
Rest.WeaviateRestClient.RestJsonSerializerOptions
792+
);
793+
return dto?.ToHFresh() ?? new VectorIndex.HFresh();
794+
}
651795
}

src/Weaviate.Client/Models/VectorIndex.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,4 +527,48 @@ public sealed record Dynamic : VectorIndexConfig
527527
[JsonIgnore]
528528
public override string Type => TypeValue;
529529
}
530+
531+
/// <summary>
532+
/// Configuration for HFresh (inverted-list ANN) vector index.
533+
/// Requires Weaviate 1.36 or later.
534+
/// </summary>
535+
public sealed record HFresh : VectorIndexConfig
536+
{
537+
/// <summary>The type discriminator string used by the Weaviate REST API.</summary>
538+
public const string TypeValue = "hfresh";
539+
540+
/// <summary>Gets or sets the distance metric for vector similarity.</summary>
541+
[JsonConverter(typeof(JsonStringEnumConverter))]
542+
public VectorDistance? Distance { get; set; }
543+
544+
/// <summary>
545+
/// Gets or sets the maximum posting list size in KB.
546+
/// When null, Weaviate computes a value based on the dataset size.
547+
/// </summary>
548+
public int? MaxPostingSizeKb { get; set; }
549+
550+
/// <summary>
551+
/// Gets or sets the number of posting lists across which vectors are distributed.
552+
/// Server default: 4.
553+
/// </summary>
554+
public int? Replicas { get; set; }
555+
556+
/// <summary>
557+
/// Gets or sets the number of posting lists probed at query time.
558+
/// Higher values improve recall at the cost of throughput. Server default: 64.
559+
/// </summary>
560+
public int? SearchProbe { get; set; }
561+
562+
/// <summary>
563+
/// Gets or sets the quantizer configuration. Only RQ is supported for HFresh.
564+
/// </summary>
565+
public QuantizerConfigBase? Quantizer { get; set; }
566+
567+
/// <summary>Gets or sets the multi-vector configuration.</summary>
568+
public MultiVectorConfig? MultiVector { get; set; }
569+
570+
/// <inheritdoc/>
571+
[JsonIgnore]
572+
public override string Type => TypeValue;
573+
}
530574
}

src/Weaviate.Client/PublicAPI.Unshipped.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const Weaviate.Client.Models.Reranker.VoyageAI.Models.RerankLite1 = "rerank-lite
5858
const Weaviate.Client.Models.Reranker.VoyageAI.TypeValue = "reranker-voyageai" -> string!
5959
const Weaviate.Client.Models.VectorIndex.Dynamic.TypeValue = "dynamic" -> string!
6060
const Weaviate.Client.Models.VectorIndex.Flat.TypeValue = "flat" -> string!
61+
const Weaviate.Client.Models.VectorIndex.HFresh.TypeValue = "hfresh" -> string!
6162
const Weaviate.Client.Models.VectorIndex.HNSW.TypeValue = "hnsw" -> string!
6263
const Weaviate.Client.Models.VectorIndex.Quantizers.BQ.TypeValue = "bq" -> string!
6364
const Weaviate.Client.Models.VectorIndex.Quantizers.None.TypeValue = "none" -> string!
@@ -146,6 +147,7 @@ override sealed Weaviate.Client.Models.TypedGuid.Equals(Weaviate.Client.Models.T
146147
override sealed Weaviate.Client.Models.TypedValue<T>.Equals(Weaviate.Client.Models.TypedBase<T>? other) -> bool
147148
override sealed Weaviate.Client.Models.VectorIndex.Dynamic.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
148149
override sealed Weaviate.Client.Models.VectorIndex.Flat.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
150+
override sealed Weaviate.Client.Models.VectorIndex.HFresh.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
149151
override sealed Weaviate.Client.Models.VectorIndex.HNSW.Equals(Weaviate.Client.Models.VectorIndexConfig? other) -> bool
150152
override sealed Weaviate.Client.Models.VectorIndex.Quantizers.BQ.Equals(Weaviate.Client.Models.VectorIndexConfig.QuantizerConfigFlat? other) -> bool
151153
override sealed Weaviate.Client.Models.VectorIndex.Quantizers.None.Equals(Weaviate.Client.Models.VectorIndexConfig.QuantizerConfigBase? other) -> bool
@@ -960,6 +962,11 @@ override Weaviate.Client.Models.VectorIndex.Flat.Equals(object? obj) -> bool
960962
override Weaviate.Client.Models.VectorIndex.Flat.GetHashCode() -> int
961963
override Weaviate.Client.Models.VectorIndex.Flat.ToString() -> string!
962964
override Weaviate.Client.Models.VectorIndex.Flat.Type.get -> string!
965+
override Weaviate.Client.Models.VectorIndex.HFresh.<Clone>$() -> Weaviate.Client.Models.VectorIndex.HFresh!
966+
override Weaviate.Client.Models.VectorIndex.HFresh.Equals(object? obj) -> bool
967+
override Weaviate.Client.Models.VectorIndex.HFresh.GetHashCode() -> int
968+
override Weaviate.Client.Models.VectorIndex.HFresh.ToString() -> string!
969+
override Weaviate.Client.Models.VectorIndex.HFresh.Type.get -> string!
963970
override Weaviate.Client.Models.VectorIndex.HNSW.<Clone>$() -> Weaviate.Client.Models.VectorIndex.HNSW!
964971
override Weaviate.Client.Models.VectorIndex.HNSW.Equals(object? obj) -> bool
965972
override Weaviate.Client.Models.VectorIndex.HNSW.GetHashCode() -> int
@@ -1814,6 +1821,8 @@ static Weaviate.Client.Models.VectorIndex.Dynamic.operator !=(Weaviate.Client.Mo
18141821
static Weaviate.Client.Models.VectorIndex.Dynamic.operator ==(Weaviate.Client.Models.VectorIndex.Dynamic? left, Weaviate.Client.Models.VectorIndex.Dynamic? right) -> bool
18151822
static Weaviate.Client.Models.VectorIndex.Flat.operator !=(Weaviate.Client.Models.VectorIndex.Flat? left, Weaviate.Client.Models.VectorIndex.Flat? right) -> bool
18161823
static Weaviate.Client.Models.VectorIndex.Flat.operator ==(Weaviate.Client.Models.VectorIndex.Flat? left, Weaviate.Client.Models.VectorIndex.Flat? right) -> bool
1824+
static Weaviate.Client.Models.VectorIndex.HFresh.operator !=(Weaviate.Client.Models.VectorIndex.HFresh? left, Weaviate.Client.Models.VectorIndex.HFresh? right) -> bool
1825+
static Weaviate.Client.Models.VectorIndex.HFresh.operator ==(Weaviate.Client.Models.VectorIndex.HFresh? left, Weaviate.Client.Models.VectorIndex.HFresh? right) -> bool
18171826
static Weaviate.Client.Models.VectorIndex.HNSW.operator !=(Weaviate.Client.Models.VectorIndex.HNSW? left, Weaviate.Client.Models.VectorIndex.HNSW? right) -> bool
18181827
static Weaviate.Client.Models.VectorIndex.HNSW.operator ==(Weaviate.Client.Models.VectorIndex.HNSW? left, Weaviate.Client.Models.VectorIndex.HNSW? right) -> bool
18191828
static Weaviate.Client.Models.VectorIndex.Quantizers.BQ.operator !=(Weaviate.Client.Models.VectorIndex.Quantizers.BQ? left, Weaviate.Client.Models.VectorIndex.Quantizers.BQ? right) -> bool
@@ -5441,6 +5450,21 @@ Weaviate.Client.Models.VectorIndex.Flat.Quantizer.get -> Weaviate.Client.Models.
54415450
Weaviate.Client.Models.VectorIndex.Flat.Quantizer.set -> void
54425451
Weaviate.Client.Models.VectorIndex.Flat.VectorCacheMaxObjects.get -> long?
54435452
Weaviate.Client.Models.VectorIndex.Flat.VectorCacheMaxObjects.set -> void
5453+
Weaviate.Client.Models.VectorIndex.HFresh
5454+
Weaviate.Client.Models.VectorIndex.HFresh.Distance.get -> Weaviate.Client.Models.VectorIndexConfig.VectorDistance?
5455+
Weaviate.Client.Models.VectorIndex.HFresh.Distance.set -> void
5456+
Weaviate.Client.Models.VectorIndex.HFresh.Equals(Weaviate.Client.Models.VectorIndex.HFresh? other) -> bool
5457+
Weaviate.Client.Models.VectorIndex.HFresh.HFresh() -> void
5458+
Weaviate.Client.Models.VectorIndex.HFresh.MaxPostingSizeKb.get -> int?
5459+
Weaviate.Client.Models.VectorIndex.HFresh.MaxPostingSizeKb.set -> void
5460+
Weaviate.Client.Models.VectorIndex.HFresh.MultiVector.get -> Weaviate.Client.Models.VectorIndexConfig.MultiVectorConfig?
5461+
Weaviate.Client.Models.VectorIndex.HFresh.MultiVector.set -> void
5462+
Weaviate.Client.Models.VectorIndex.HFresh.Quantizer.get -> Weaviate.Client.Models.VectorIndexConfig.QuantizerConfigBase?
5463+
Weaviate.Client.Models.VectorIndex.HFresh.Quantizer.set -> void
5464+
Weaviate.Client.Models.VectorIndex.HFresh.Replicas.get -> int?
5465+
Weaviate.Client.Models.VectorIndex.HFresh.Replicas.set -> void
5466+
Weaviate.Client.Models.VectorIndex.HFresh.SearchProbe.get -> int?
5467+
Weaviate.Client.Models.VectorIndex.HFresh.SearchProbe.set -> void
54445468
Weaviate.Client.Models.VectorIndex.HNSW
54455469
Weaviate.Client.Models.VectorIndex.HNSW.CleanupIntervalSeconds.get -> int?
54465470
Weaviate.Client.Models.VectorIndex.HNSW.CleanupIntervalSeconds.set -> void

0 commit comments

Comments
 (0)