diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs b/src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs index 304214f1..0c2d0614 100644 --- a/src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs +++ b/src/Weaviate.Client.Tests/Unit/TestVectorIndexConfig.cs @@ -120,4 +120,83 @@ public void VectorIndexConfig_SkipDefaultQuantization_Deserializes_To_None_Quant Assert.Equal("none", hnsw?.Quantizer.Type); Assert.IsType(hnsw?.Quantizer); } + + /// + /// Tests that vector index config hfresh deserializes from json + /// + [Fact] + public void VectorIndexConfig_HFresh_From_Json() + { + var json = + @"{ ""distance"": ""cosine"", ""maxPostingSizeKB"": 256, ""replicas"": 4, ""searchProbe"": 64 }"; + + var config = JsonSerializer.Deserialize>( + 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); + } + + /// + /// Tests that vector index config hfresh roundtrips through serialization + /// + [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>( + 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); + } + + /// + /// Tests that vector index config hfresh preserves RQ quantizer through serialization + /// + [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>( + json, + Weaviate.Client.Rest.WeaviateRestClient.RestJsonSerializerOptions + ); + var roundtripped = (VectorIndex.HFresh?)VectorIndexSerialization.Factory("hfresh", dict); + + Assert.NotNull(roundtripped?.Quantizer); + var rq = Assert.IsType(roundtripped?.Quantizer); + Assert.Equal("rq", rq.Type); + Assert.Equal(8, rq.Bits); + Assert.Equal(20, rq.RescoreLimit); + } } diff --git a/src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs b/src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs index 6fbc1a8e..2540a742 100644 --- a/src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs +++ b/src/Weaviate.Client/Models/Serialization.VectorIndexConfig.cs @@ -273,6 +273,50 @@ internal class DynamicDto public FlatDto? Flat { get; set; } } +/// +/// The hfresh dto class +/// +internal class HFreshDto +{ + /// + /// Gets or sets the value of the distance + /// + [JsonPropertyName("distance")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public VectorDistance? Distance { get; set; } + + /// + /// Gets or sets the maximum posting list size in KB. + /// Note: JSON key uses uppercase "KB" per Weaviate API convention. + /// + [JsonPropertyName("maxPostingSizeKB")] + public int? MaxPostingSizeKb { get; set; } + + /// + /// Gets or sets the value of the replicas + /// + [JsonPropertyName("replicas")] + public int? Replicas { get; set; } + + /// + /// Gets or sets the value of the search probe + /// + [JsonPropertyName("searchProbe")] + public int? SearchProbe { get; set; } + + /// + /// Gets or sets the RQ quantizer. Only RQ is supported for HFresh. + /// + [JsonPropertyName("rq")] + public VectorIndex.Quantizers.RQ? RQ { get; set; } + + /// + /// Gets or sets the value of the multi vector + /// + [JsonPropertyName("multivector")] + public MultiVectorDto? MultiVector { get; set; } +} + // Extension methods for mapping /// /// The vector index mapping extensions class @@ -526,6 +570,79 @@ public static DynamicDto ToDto(this VectorIndex.Dynamic dynamic) Flat = dynamic.Flat?.ToDto(), }; } + + // HFresh mapping + /// + /// Returns the hfresh using the specified dto + /// + /// The dto + /// The vector index hfresh + 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, + }; + } + + /// + /// Returns the dto using the specified hfresh + /// + /// The hfresh + /// The hfresh dto + 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, + }, + }, + }; + } } /// @@ -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, }; @@ -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, }; @@ -648,4 +767,29 @@ public static VectorIndex.Dynamic DeserializeDynamic(IDictionary + /// Serializes the hfresh using the specified hfresh + /// + /// The hfresh + /// The string + public static string SerializeHFresh(VectorIndex.HFresh hfresh) + { + var dto = hfresh.ToDto(); + return JsonSerializer.Serialize(dto, Rest.WeaviateRestClient.RestJsonSerializerOptions); + } + + /// + /// Deserializes the hfresh using the specified json + /// + /// The json + /// The vector index hfresh + public static VectorIndex.HFresh DeserializeHFresh(IDictionary json) + { + var dto = JsonSerializer.Deserialize( + JsonSerializer.Serialize(json, Rest.WeaviateRestClient.RestJsonSerializerOptions), + Rest.WeaviateRestClient.RestJsonSerializerOptions + ); + return dto?.ToHFresh() ?? new VectorIndex.HFresh(); + } } diff --git a/src/Weaviate.Client/Models/VectorIndex.cs b/src/Weaviate.Client/Models/VectorIndex.cs index 11e5a6a5..802f6147 100644 --- a/src/Weaviate.Client/Models/VectorIndex.cs +++ b/src/Weaviate.Client/Models/VectorIndex.cs @@ -527,4 +527,48 @@ public sealed record Dynamic : VectorIndexConfig [JsonIgnore] public override string Type => TypeValue; } + + /// + /// Configuration for HFresh (inverted-list ANN) vector index. + /// Requires Weaviate 1.36 or later. + /// + public sealed record HFresh : VectorIndexConfig + { + /// The type discriminator string used by the Weaviate REST API. + public const string TypeValue = "hfresh"; + + /// Gets or sets the distance metric for vector similarity. + [JsonConverter(typeof(JsonStringEnumConverter))] + public VectorDistance? Distance { get; set; } + + /// + /// Gets or sets the maximum posting list size in KB. + /// When null, Weaviate computes a value based on the dataset size. + /// + public int? MaxPostingSizeKb { get; set; } + + /// + /// Gets or sets the number of posting lists across which vectors are distributed. + /// Server default: 4. + /// + public int? Replicas { get; set; } + + /// + /// Gets or sets the number of posting lists probed at query time. + /// Higher values improve recall at the cost of throughput. Server default: 64. + /// + public int? SearchProbe { get; set; } + + /// + /// Gets or sets the quantizer configuration. Only RQ is supported for HFresh. + /// + public QuantizerConfigBase? Quantizer { get; set; } + + /// Gets or sets the multi-vector configuration. + public MultiVectorConfig? MultiVector { get; set; } + + /// + [JsonIgnore] + public override string Type => TypeValue; + } } diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index babea8f8..4dc05547 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -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! @@ -146,6 +147,7 @@ override sealed Weaviate.Client.Models.TypedGuid.Equals(Weaviate.Client.Models.T override sealed Weaviate.Client.Models.TypedValue.Equals(Weaviate.Client.Models.TypedBase? 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 @@ -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.$() -> 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.$() -> Weaviate.Client.Models.VectorIndex.HNSW! override Weaviate.Client.Models.VectorIndex.HNSW.Equals(object? obj) -> bool override Weaviate.Client.Models.VectorIndex.HNSW.GetHashCode() -> int @@ -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 @@ -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