├── icon.png ├── .gitignore ├── src └── Typesense │ ├── ImportType.cs │ ├── DeleteKeyResponse.cs │ ├── HttpContents │ ├── Bytes.cs │ ├── StreamStringLinesHttpContent.cs │ └── StreamJsonLinesHttpContent.cs │ ├── HealthResponse.cs │ ├── DeleteSynonymResponse.cs │ ├── SnapshotResponse.cs │ ├── CompactDiskResponse.cs │ ├── DeleteSearchOverrideResponse.cs │ ├── FilterUpdateResponse.cs │ ├── FilterDeleteResponse.cs │ ├── TruncateCollectionResponse.cs │ ├── ExportParameters.cs │ ├── ListKeysResponse.cs │ ├── UpdateCollectionResponse.cs │ ├── ListSynonymsResponse.cs │ ├── CollectionAliasResponse.cs │ ├── ListSearchOverridesResponse.cs │ ├── ListCollectionAliasesResponse.cs │ ├── CollectionAlias.cs │ ├── SynonymSchema.cs │ ├── Setup │ ├── Config.cs │ ├── TypesenseExtension.cs │ └── Node.cs │ ├── ImportResponse.cs │ ├── Converter │ ├── UnixEpochDateTimeConverter.cs │ ├── VectorQueryConverter.cs │ ├── UnixEpochDateTimeLongConverter.cs │ ├── GroupKeyConverter.cs │ ├── MatchedTokenConverter.cs │ └── JsonStringEnumConverter.cs │ ├── SynonymSchemaResponse.cs │ ├── FieldType.cs │ ├── Key.cs │ ├── Typesense.csproj │ ├── KeyResponse.cs │ ├── AutoEmbeddingConfig.cs │ ├── Schema.cs │ ├── CollectionResponse.cs │ ├── SearchOverrideResponse.cs │ ├── StatsResponse.cs │ ├── UpdateSchema.cs │ ├── TypesenseApiException.cs │ ├── SearchOverride.cs │ ├── MetricsResponse.cs │ ├── Field.cs │ ├── VectorSearchQuery.cs │ ├── SearchResult.cs │ ├── SearchParameters.cs │ └── TypesenseClient.cs ├── examples └── Example │ ├── Address.cs │ ├── Example.csproj │ └── Program.cs ├── test └── Typesense.Tests │ ├── Typesense.Tests.csproj │ ├── PriorityOrderer.cs │ ├── TypesenseFixture.cs │ └── TypesenseConverterTests.cs ├── LICENSE ├── .editorconfig ├── .circleci └── config.yml ├── typesense-dotnet.sln └── README.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DAXGRID/typesense-dotnet/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### DotnetCore ### 2 | # .NET Core build folders 3 | bin/ 4 | obj/ 5 | .vscode 6 | .cache 7 | .vs -------------------------------------------------------------------------------- /src/Typesense/ImportType.cs: -------------------------------------------------------------------------------- 1 | namespace Typesense; 2 | 3 | public enum ImportType 4 | { 5 | Create, 6 | Upsert, 7 | Update, 8 | Emplace 9 | } 10 | -------------------------------------------------------------------------------- /src/Typesense/DeleteKeyResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record DeleteKeyResponse 6 | { 7 | [JsonPropertyName("id")] 8 | public int Id { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Typesense/HttpContents/Bytes.cs: -------------------------------------------------------------------------------- 1 | namespace Typesense.HttpContents; 2 | /// To avoid warnings in the generic class 3 | public static class Bytes 4 | { 5 | public static readonly byte[] NewLine = { (byte)'\n' }; 6 | } -------------------------------------------------------------------------------- /examples/Example/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Example; 2 | 3 | class Address 4 | { 5 | public string Id { get; set; } 6 | public int HouseNumber { get; set; } 7 | public string AccessAddress { get; set; } 8 | public string MetadataNotes { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Typesense/HealthResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public sealed record HealthResponse 6 | { 7 | [JsonPropertyName("ok")] 8 | public bool Ok { get; init; } 9 | 10 | [JsonConstructor] 11 | public HealthResponse(bool ok) 12 | { 13 | Ok = ok; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/DeleteSynonymResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record DeleteSynonymResponse 6 | { 7 | [JsonPropertyName("id")] 8 | public string Id { get; init; } 9 | 10 | [JsonConstructor] 11 | public DeleteSynonymResponse(string id) 12 | { 13 | Id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/SnapshotResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record SnapshotResponse 6 | { 7 | [JsonPropertyName("success")] 8 | public bool Success { get; init; } 9 | 10 | [JsonConstructor] 11 | public SnapshotResponse(bool success) 12 | { 13 | Success = success; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Typesense/CompactDiskResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record CompactDiskResponse 6 | { 7 | [JsonPropertyName("success")] 8 | public bool Success { get; init; } 9 | 10 | [JsonConstructor] 11 | public CompactDiskResponse(bool success) 12 | { 13 | Success = success; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Typesense/DeleteSearchOverrideResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record DeleteSearchOverrideResponse 6 | { 7 | [JsonPropertyName("id")] 8 | public string Id { get; init; } 9 | 10 | [JsonConstructor] 11 | public DeleteSearchOverrideResponse(string id) 12 | { 13 | Id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/FilterUpdateResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record FilterUpdateResponse 6 | { 7 | [JsonPropertyName("num_updated")] 8 | public int NumberUpdated { get; init; } 9 | 10 | [JsonConstructor] 11 | public FilterUpdateResponse(int numberUpdated) 12 | { 13 | NumberUpdated = numberUpdated; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/FilterDeleteResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record FilterDeleteResponse 6 | { 7 | [JsonPropertyName("num_deleted")] 8 | public int NumberOfDeleted { get; init; } 9 | 10 | [JsonConstructor] 11 | public FilterDeleteResponse(int numberOfDeleted) 12 | { 13 | NumberOfDeleted = numberOfDeleted; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/TruncateCollectionResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record TruncateCollectionResponse 6 | { 7 | [JsonPropertyName("num_deleted")] 8 | public int NumDeleted { get; init; } 9 | 10 | [JsonConstructor] 11 | public TruncateCollectionResponse(int numDeleted) 12 | { 13 | NumDeleted = numDeleted; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/ExportParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record ExportParameters 6 | { 7 | [JsonPropertyName("filter_by")] 8 | public string? FilterBy { get; set; } 9 | 10 | [JsonPropertyName("include_fields")] 11 | public string? IncludeFields { get; set; } 12 | 13 | [JsonPropertyName("exclude_fields")] 14 | public string? ExcludeFields { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Typesense/ListKeysResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record ListKeysResponse 7 | { 8 | [JsonPropertyName("keys")] 9 | public IReadOnlyCollection Keys { get; init; } 10 | 11 | [JsonConstructor] 12 | public ListKeysResponse(IReadOnlyCollection keys) 13 | { 14 | Keys = keys; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Typesense/UpdateCollectionResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record UpdateCollectionResponse 7 | { 8 | [JsonPropertyName("fields")] 9 | public IReadOnlyList Fields { get; init; } 10 | 11 | [JsonConstructor] 12 | public UpdateCollectionResponse(IReadOnlyList fields) 13 | { 14 | Fields = fields; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/Example/Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Exe 13 | net8.0 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Typesense/ListSynonymsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record ListSynonymsResponse 7 | { 8 | [JsonPropertyName("synonyms")] 9 | public IReadOnlyCollection Synonyms { get; init; } 10 | 11 | [JsonConstructor] 12 | public ListSynonymsResponse(IReadOnlyCollection synonyms) 13 | { 14 | Synonyms = synonyms; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Typesense/CollectionAliasResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record CollectionAliasResponse 6 | { 7 | [JsonPropertyName("collection_name")] 8 | public string CollectionName { get; init; } 9 | 10 | [JsonPropertyName("name")] 11 | public string Name { get; init; } 12 | 13 | [JsonConstructor] 14 | public CollectionAliasResponse(string collectionName, string name) 15 | { 16 | CollectionName = collectionName; 17 | Name = name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Typesense/ListSearchOverridesResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record ListSearchOverridesResponse 7 | { 8 | [JsonPropertyName("overrides")] 9 | public IReadOnlyCollection SearchOverrides { get; init; } 10 | 11 | [JsonConstructor] 12 | public ListSearchOverridesResponse(IReadOnlyCollection searchOverrides) 13 | { 14 | SearchOverrides = searchOverrides; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Typesense/ListCollectionAliasesResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record ListCollectionAliasesResponse 7 | { 8 | [JsonPropertyName("aliases")] 9 | public IReadOnlyCollection CollectionAliases { get; init; } 10 | 11 | [JsonConstructor] 12 | public ListCollectionAliasesResponse(IReadOnlyCollection collectionAliases) 13 | { 14 | CollectionAliases = collectionAliases; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Typesense/CollectionAlias.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record CollectionAlias 7 | { 8 | [JsonPropertyName("collection_name")] 9 | public string CollectionName { get; init; } 10 | 11 | public CollectionAlias(string collectionName) 12 | { 13 | if (string.IsNullOrWhiteSpace(collectionName)) 14 | throw new ArgumentException( 15 | $"{nameof(collectionName)} cannot be null, empty or whitespace."); 16 | CollectionName = collectionName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Typesense/SynonymSchema.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record SynonymSchema 7 | { 8 | [JsonPropertyName("synonyms")] 9 | public IEnumerable Synonyms { get; init; } 10 | 11 | [JsonPropertyName("root")] 12 | public string? Root { get; init; } 13 | 14 | [JsonPropertyName("symbols_to_index")] 15 | public IEnumerable? SymbolsToIndex { get; init; } 16 | 17 | public SynonymSchema(IEnumerable synonyms) 18 | { 19 | Synonyms = synonyms; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Typesense/Setup/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | 5 | namespace Typesense.Setup; 6 | 7 | public record Config 8 | { 9 | public IReadOnlyCollection Nodes { get; set; } 10 | public string ApiKey { get; set; } 11 | public JsonSerializerOptions? JsonSerializerOptions { get; set; } 12 | 13 | [Obsolete("Use multi-arity constructor instead.")] 14 | public Config() 15 | { 16 | Nodes = new List(); 17 | ApiKey = ""; 18 | } 19 | 20 | public Config(IReadOnlyCollection nodes, string apiKey) 21 | { 22 | Nodes = nodes; 23 | ApiKey = apiKey; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Typesense/ImportResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public record ImportResponse 6 | { 7 | [JsonPropertyName("success")] 8 | public bool Success { get; init; } 9 | 10 | [JsonPropertyName("error")] 11 | public string? Error { get; init; } 12 | 13 | [JsonPropertyName("document")] 14 | public string? Document { get; init; } 15 | 16 | [JsonPropertyName("id")] 17 | public string? Id { get; init; } 18 | 19 | [JsonConstructor] 20 | public ImportResponse(bool success, string? error = null, string? document = null, string? id = null) 21 | { 22 | Success = success; 23 | Error = error; 24 | Document = document; 25 | Id = id; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Typesense/Converter/UnixEpochDateTimeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Typesense.Converter; 7 | 8 | public class UnixEpochDateTimeConverter : JsonConverter 9 | { 10 | public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | return DateTime.UnixEpoch.AddSeconds(reader.GetInt64()); 13 | } 14 | 15 | public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) 16 | { 17 | ArgumentNullException.ThrowIfNull(writer); 18 | writer.WriteStringValue((value - DateTime.UnixEpoch).TotalSeconds.ToString(CultureInfo.InvariantCulture)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Typesense/SynonymSchemaResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record SynonymSchemaResponse 7 | { 8 | [JsonPropertyName("id")] 9 | public string Id { get; init; } 10 | 11 | [JsonPropertyName("synonyms")] 12 | public IReadOnlyCollection Synonyms { get; init; } 13 | 14 | [JsonPropertyName("root")] 15 | public string Root { get; init; } 16 | 17 | [JsonPropertyName("symbols_to_index")] 18 | public IReadOnlyCollection SymbolsToIndex { get; init; } 19 | 20 | [JsonConstructor] 21 | public SynonymSchemaResponse(string id, 22 | IReadOnlyCollection synonyms, 23 | string root, 24 | IReadOnlyCollection symbolsToIndex) 25 | { 26 | Id = id; 27 | Synonyms = synonyms; 28 | Root = root; 29 | SymbolsToIndex = symbolsToIndex; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Typesense/Converter/VectorQueryConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Typesense.Converter; 6 | 7 | public class VectorQueryJsonConverter : JsonConverter 8 | { 9 | public override bool CanConvert(Type typeToConvert) 10 | { 11 | return typeToConvert == typeof(VectorQuery); 12 | } 13 | 14 | public override VectorQuery Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 15 | { 16 | var jsonValue = reader.GetString() ?? string.Empty; 17 | return !String.IsNullOrEmpty(jsonValue) ? new VectorQuery(jsonValue) : null!; 18 | } 19 | 20 | public override void Write(Utf8JsonWriter writer, VectorQuery value, JsonSerializerOptions options) 21 | { 22 | ArgumentNullException.ThrowIfNull(writer); 23 | ArgumentNullException.ThrowIfNull(value); 24 | 25 | writer.WriteStringValue(value.ToQuery()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/Typesense.Tests/Typesense.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DAX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Typesense/Converter/UnixEpochDateTimeLongConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Typesense.Converter; 6 | 7 | /// 8 | /// Converts between nullable DateTime and Unix epoch seconds as a long integer value. 9 | /// 10 | public class UnixEpochDateTimeLongConverter : JsonConverter 11 | { 12 | public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 13 | { 14 | if (reader.TokenType == JsonTokenType.Null) 15 | return null; 16 | 17 | return DateTime.UnixEpoch.AddSeconds(reader.GetInt64()); 18 | } 19 | 20 | public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) 21 | { 22 | ArgumentNullException.ThrowIfNull(writer); 23 | 24 | if (value == null) 25 | { 26 | writer.WriteNullValue(); 27 | return; 28 | } 29 | 30 | writer.WriteNumberValue(Convert.ToInt64((value.Value - DateTime.UnixEpoch).TotalSeconds)); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Typesense/FieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public enum FieldType 6 | { 7 | [EnumMember(Value = "string")] 8 | String, 9 | 10 | [EnumMember(Value = "string[]")] 11 | StringArray, 12 | 13 | [EnumMember(Value = "int32")] 14 | Int32, 15 | 16 | [EnumMember(Value = "int32[]")] 17 | Int32Array, 18 | 19 | [EnumMember(Value = "int64")] 20 | Int64, 21 | 22 | [EnumMember(Value = "int64[]")] 23 | Int64Array, 24 | 25 | [EnumMember(Value = "float")] 26 | Float, 27 | 28 | [EnumMember(Value = "float[]")] 29 | FloatArray, 30 | 31 | [EnumMember(Value = "bool")] 32 | Bool, 33 | 34 | [EnumMember(Value = "bool[]")] 35 | BoolArray, 36 | 37 | [EnumMember(Value = "geopoint")] 38 | GeoPoint, 39 | 40 | [EnumMember(Value = "geopoint[]")] 41 | GeoPointArray, 42 | 43 | [EnumMember(Value = "object")] 44 | Object, 45 | 46 | [EnumMember(Value = "object[]")] 47 | ObjectArray, 48 | 49 | [EnumMember(Value = "string*")] 50 | AutoString, 51 | 52 | [EnumMember(Value = "auto")] 53 | Auto, 54 | 55 | [EnumMember(Value = "image")] 56 | Image, 57 | } 58 | -------------------------------------------------------------------------------- /src/Typesense/Key.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Typesense; 6 | 7 | public record Key 8 | { 9 | [JsonPropertyName("actions")] 10 | public IReadOnlyCollection Actions { get; init; } 11 | 12 | [JsonPropertyName("description")] 13 | public string Description { get; init; } 14 | 15 | [JsonPropertyName("collections")] 16 | public IReadOnlyCollection Collections { get; init; } 17 | 18 | [JsonPropertyName("value")] 19 | public string? Value { get; init; } 20 | 21 | [JsonPropertyName("expires_at")] 22 | public long? ExpiresAt { get; init; } 23 | 24 | [JsonPropertyName("autodelete")] 25 | public bool? AutoDelete { get; set; } 26 | 27 | [Obsolete("Use multi-arity constructor instead.")] 28 | public Key() 29 | { 30 | Actions = new List(); 31 | Description = string.Empty; 32 | Collections = new List(); 33 | } 34 | 35 | public Key( 36 | string description, 37 | IReadOnlyCollection actions, 38 | IReadOnlyCollection collections) 39 | { 40 | Actions = actions; 41 | Description = description; 42 | Collections = collections; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Typesense/Typesense.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0; 5 | true 6 | true 7 | All 8 | enable 9 | 10 | Typesense 11 | .NET HTTP client for Typesense. 12 | typesense;search;search-engine;fuzzy-search;instant-search 13 | icon.png 14 | git 15 | https://github.com/DAXGRID/typesense-dotnet 16 | MIT 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Typesense/KeyResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record KeyResponse 7 | { 8 | [JsonPropertyName("id")] 9 | public int Id { get; init; } 10 | 11 | [JsonPropertyName("value")] 12 | public string Value { get; init; } 13 | 14 | [JsonPropertyName("value_prefix")] 15 | public string ValuePrefix { get; init; } 16 | 17 | [JsonPropertyName("description")] 18 | public string? Description { get; init; } 19 | 20 | [JsonPropertyName("actions")] 21 | public IReadOnlyCollection? Actions { get; init; } 22 | 23 | [JsonPropertyName("collections")] 24 | public IReadOnlyCollection? Collections { get; init; } 25 | 26 | [JsonPropertyName("expires_at")] 27 | public long ExpiresAt { get; init; } 28 | 29 | [JsonConstructor] 30 | public KeyResponse( 31 | int id, 32 | string value, 33 | string valuePrefix, 34 | long expiresAt, 35 | string? description = null, 36 | IReadOnlyCollection? actions = null, 37 | IReadOnlyCollection? collections = null) 38 | { 39 | Id = id; 40 | Value = value; 41 | ValuePrefix = valuePrefix; 42 | ExpiresAt = expiresAt; 43 | Description = description; 44 | Actions = actions; 45 | Collections = collections; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | #dotnet_analyzer_diagnostic.severity = error 3 | 4 | # IDE0007 and IDE0008 'var' preferences 5 | csharp_style_var_for_built_in_types = true 6 | csharp_style_var_when_type_is_apparent = true 7 | csharp_style_var_elsewhere = true 8 | 9 | # IDE0011 Add braces 10 | csharp_prefer_braces = false 11 | 12 | # IDE0022 Use expression body for methods 13 | csharp_style_expression_bodied_methods = when_on_single_line 14 | 15 | # IDE0160: Convert to file-scoped namespace 16 | csharp_style_namespace_declarations = file_scoped:error 17 | 18 | # IDE0058: Expression value is never used 19 | dotnet_diagnostic.CA1707.severity = silent 20 | 21 | # CA1014: Mark assemblies with CLSCompliantAttribute 22 | dotnet_diagnostic.CA1014.severity = none 23 | 24 | # CA2007: Do not directly await a Task 25 | dotnet_diagnostic.CA2007.severity = silent 26 | 27 | # CA1720: Identifiers should not contain type names 28 | dotnet_diagnostic.CA1720.severity = silent 29 | 30 | # CA1724: Type names should not match namespaces 31 | dotnet_diagnostic.CA1724.severity = silent 32 | 33 | # CA1711: Identifiers should not have incorrect suffix 34 | dotnet_diagnostic.CA1711.severity = silent 35 | 36 | # CA1308: Normalize strings to uppercase 37 | dotnet_diagnostic.CA1308.severity = silent 38 | 39 | # CA2234: Pass System.Uri objects instead of strings 40 | dotnet_diagnostic.CA2234.severity = silent 41 | 42 | 43 | # CS1591: Ignored for now, until we decide to have comments on everything 44 | dotnet_diagnostic.CS1591.severity = silent 45 | 46 | [test/**.cs] 47 | # IDE0058 Remove unnecessary expression value 48 | dotnet_diagnostic.IDE0058.severity = silent 49 | -------------------------------------------------------------------------------- /src/Typesense/Converter/GroupKeyConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | 8 | namespace Typesense.Converter; 9 | 10 | public class GroupKeyConverter : JsonConverter> 11 | { 12 | public override IReadOnlyList? Read(ref Utf8JsonReader reader, Type typeToConvert, 13 | JsonSerializerOptions options) 14 | { 15 | var jsonDocument = JsonDocument.ParseValue(ref reader); 16 | 17 | return jsonDocument.RootElement.EnumerateArray().Select(StringifyJsonElement).ToList(); 18 | } 19 | 20 | private static string StringifyJsonElement(JsonElement element) 21 | { 22 | var elementValue = element.ValueKind switch 23 | { 24 | JsonValueKind.String => element.GetString(), 25 | JsonValueKind.False => "false", 26 | JsonValueKind.True => "true", 27 | JsonValueKind.Number => element.GetDecimal().ToString(CultureInfo.CreateSpecificCulture("en-US")), 28 | JsonValueKind.Array => string.Join(", ", element.EnumerateArray().Select(StringifyJsonElement)), 29 | _ => null 30 | }; 31 | 32 | if (elementValue is null) 33 | throw new InvalidOperationException($"{nameof(elementValue)} being null is invalid."); 34 | 35 | return elementValue; 36 | } 37 | 38 | public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) 39 | { 40 | JsonSerializer.Serialize(writer, value); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Typesense/AutoEmbeddingConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record AutoEmbeddingConfig 7 | { 8 | [JsonPropertyName("from")] 9 | public Collection From { get; init; } 10 | 11 | [JsonPropertyName("model_config")] 12 | public ModelConfig ModelConfig { get; init; } 13 | 14 | public AutoEmbeddingConfig(Collection from, ModelConfig modelConfig) 15 | { 16 | From = from; 17 | ModelConfig = modelConfig; 18 | } 19 | } 20 | 21 | public record ModelConfig 22 | { 23 | [JsonPropertyName("model_name")] 24 | public string ModelName { get; init; } 25 | 26 | [JsonPropertyName("api_key")] 27 | public string? ApiKey { get; init; } 28 | 29 | [JsonPropertyName("url")] 30 | public string? Url { get; init; } 31 | 32 | [JsonPropertyName("access_token")] 33 | public string? AccessToken { get; init; } 34 | 35 | [JsonPropertyName("refresh_token")] 36 | public string? RefreshToken { get; init; } 37 | 38 | [JsonPropertyName("client_id")] 39 | public string? ClientId { get; init; } 40 | 41 | [JsonPropertyName("client_secret")] 42 | public string? ClientSecret { get; init; } 43 | 44 | [JsonPropertyName("project_id")] 45 | public string? ProjectId { get; init; } 46 | 47 | [JsonPropertyName("indexing_prefix")] 48 | public string? IndexingPrefix { get; init; } 49 | 50 | [JsonPropertyName("query_prefix")] 51 | public string? QueryPrefix { get; init; } 52 | 53 | public ModelConfig(string modelName) 54 | { 55 | ModelName = modelName; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Typesense/Schema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Typesense; 6 | 7 | public record Schema 8 | { 9 | [JsonPropertyName("name")] 10 | public string Name { get; init; } 11 | 12 | [JsonPropertyName("fields")] 13 | public IEnumerable Fields { get; init; } 14 | 15 | [JsonPropertyName("default_sorting_field")] 16 | public string? DefaultSortingField { get; init; } 17 | 18 | [JsonPropertyName("token_separators")] 19 | public IEnumerable? TokenSeparators { get; init; } 20 | 21 | [JsonPropertyName("symbols_to_index")] 22 | public IEnumerable? SymbolsToIndex { get; init; } 23 | 24 | [JsonPropertyName("enable_nested_fields")] 25 | public bool? EnableNestedFields { get; init; } 26 | 27 | [JsonPropertyName("metadata")] 28 | public IDictionary? Metadata { get; init; } 29 | 30 | [Obsolete("Use multi-arity constructor instead.")] 31 | public Schema() 32 | { 33 | Name = ""; 34 | Fields = new List(); 35 | } 36 | 37 | public Schema(string name, IEnumerable fields) 38 | { 39 | if (string.IsNullOrWhiteSpace(name)) 40 | throw new ArgumentNullException(nameof(name)); 41 | 42 | Name = name; 43 | Fields = fields; 44 | } 45 | 46 | public Schema(string name, IEnumerable fields, string defaultSortingField) 47 | { 48 | if (string.IsNullOrWhiteSpace(name)) 49 | throw new ArgumentNullException(name); 50 | 51 | Name = name; 52 | Fields = fields; 53 | DefaultSortingField = defaultSortingField; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/Typesense.Tests/PriorityOrderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xunit.Abstractions; 5 | using Xunit.Sdk; 6 | 7 | namespace Typesense.Tests; 8 | 9 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 10 | public class TestPriorityAttribute : Attribute 11 | { 12 | public int Priority { get; private set; } 13 | 14 | public TestPriorityAttribute(int priority) => Priority = priority; 15 | } 16 | 17 | public class PriorityOrderer : ITestCaseOrderer 18 | { 19 | public IEnumerable OrderTestCases( 20 | IEnumerable testCases) where TTestCase : ITestCase 21 | { 22 | string assemblyName = typeof(TestPriorityAttribute).AssemblyQualifiedName!; 23 | var sortedMethods = new SortedDictionary>(); 24 | foreach (TTestCase testCase in testCases) 25 | { 26 | int priority = testCase.TestMethod.Method 27 | .GetCustomAttributes(assemblyName) 28 | .FirstOrDefault() 29 | ?.GetNamedArgument(nameof(TestPriorityAttribute.Priority)) ?? 0; 30 | 31 | GetOrCreate(sortedMethods, priority).Add(testCase); 32 | } 33 | 34 | foreach (TTestCase testCase in 35 | sortedMethods.Keys.SelectMany( 36 | priority => sortedMethods[priority].OrderBy( 37 | testCase => testCase.TestMethod.Method.Name))) 38 | { 39 | yield return testCase; 40 | } 41 | } 42 | 43 | private static TValue GetOrCreate( 44 | IDictionary dictionary, TKey key) 45 | where TKey : struct 46 | where TValue : new() => 47 | dictionary.TryGetValue(key, out TValue result) 48 | ? result 49 | : (dictionary[key] = new TValue()); 50 | } 51 | -------------------------------------------------------------------------------- /src/Typesense/Converter/MatchedTokenConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Typesense.Converter; 7 | 8 | public class MatchedTokenConverter : JsonConverter> 9 | { 10 | public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | var jsonDocument = JsonDocument.ParseValue(ref reader); 13 | var matchedTokens = new List(); 14 | 15 | foreach (var element in jsonDocument.RootElement.EnumerateArray()) 16 | { 17 | if (element.ValueKind == JsonValueKind.String) 18 | { 19 | var elementValue = element.GetString(); 20 | if (elementValue is null) 21 | throw new InvalidOperationException($"{nameof(elementValue)} being null is invalid."); 22 | 23 | matchedTokens.Add(elementValue); 24 | } 25 | else if (element.ValueKind == JsonValueKind.Array) 26 | { 27 | var elements = new List(); 28 | foreach (var stringElement in element.EnumerateArray()) 29 | { 30 | var elementValue = stringElement.GetString(); 31 | if (elementValue is null) 32 | throw new InvalidOperationException($"{nameof(elementValue)} being null is invalid."); 33 | 34 | elements.Add(elementValue); 35 | } 36 | 37 | matchedTokens.Add(elements); 38 | } 39 | } 40 | 41 | return matchedTokens; 42 | } 43 | 44 | public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) 45 | { 46 | JsonSerializer.Serialize(writer, value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Typesense/Setup/TypesenseExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | 6 | namespace Typesense.Setup; 7 | 8 | public static class TypesenseExtension 9 | { 10 | /// 11 | /// The collection of services to be configured for the Typesense client. 12 | /// 13 | /// 14 | /// The configuration for the Typesense client. 15 | /// 16 | /// 17 | /// If set to true, HTTP compression is enabled, lowering response times and reducing traffic for externally hosted Typesense, like Typesense Cloud 18 | /// Set to false by default to mimic the old behavior, and not add compression processing overhead on locally hosted Typesense 19 | /// 20 | /// 21 | public static IServiceCollection AddTypesenseClient(this IServiceCollection serviceCollection, Action config, bool enableHttpCompression = false) 22 | { 23 | if (config == null) 24 | throw new ArgumentNullException(nameof(config), $"Please provide options for TypesenseClient."); 25 | 26 | var httpClientBuilder = serviceCollection 27 | .AddScoped() 28 | .AddHttpClient(client => 29 | { 30 | client.DefaultRequestVersion = HttpVersion.Version30; 31 | }); 32 | if (enableHttpCompression) 33 | httpClientBuilder = httpClientBuilder 34 | .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler 35 | { 36 | AutomaticDecompression = DecompressionMethods.All 37 | }); 38 | return httpClientBuilder.Services 39 | .Configure(config); 40 | } 41 | } -------------------------------------------------------------------------------- /test/Typesense.Tests/TypesenseFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Typesense.Setup; 5 | using Xunit; 6 | 7 | namespace Typesense.Tests; 8 | 9 | public class TypesenseFixture : IAsyncLifetime 10 | { 11 | public ITypesenseClient Client => GetClient(); 12 | 13 | public async Task InitializeAsync() 14 | { 15 | await Task.WhenAll( 16 | CleanCollections(), 17 | CleanApiKeys(), 18 | CleanAlias()); 19 | } 20 | 21 | private async Task CleanCollections() 22 | { 23 | var collections = await Client.RetrieveCollections(); 24 | foreach (var collection in collections) 25 | { 26 | await Client.DeleteCollection(collection.Name); 27 | } 28 | } 29 | 30 | private async Task CleanApiKeys() 31 | { 32 | var apiKeys = await Client.ListKeys(); 33 | foreach (var key in apiKeys.Keys) 34 | { 35 | await Client.DeleteKey(key.Id); 36 | } 37 | } 38 | 39 | private async Task CleanAlias() 40 | { 41 | var aliases = await Client.ListCollectionAliases(); 42 | foreach (var alias in aliases.CollectionAliases) 43 | { 44 | await Client.DeleteCollectionAlias(alias.Name); 45 | } 46 | } 47 | 48 | private ITypesenseClient GetClient() 49 | { 50 | return new ServiceCollection() 51 | .AddTypesenseClient(config => 52 | { 53 | config.ApiKey = "key"; 54 | config.Nodes = new List 55 | { 56 | new Node("localhost", "8108", "http") 57 | }; 58 | }, enableHttpCompression: true).BuildServiceProvider().GetService(); 59 | } 60 | 61 | public Task DisposeAsync() 62 | { 63 | return Task.CompletedTask; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Typesense/CollectionResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | using Typesense.Converter; 5 | 6 | namespace Typesense; 7 | 8 | public record CollectionResponse 9 | { 10 | [JsonPropertyName("name")] 11 | public string Name { get; init; } 12 | 13 | [JsonPropertyName("num_documents")] 14 | public int NumberOfDocuments { get; init; } 15 | 16 | [JsonPropertyName("fields")] 17 | public IReadOnlyCollection Fields { get; init; } 18 | 19 | [JsonPropertyName("default_sorting_field")] 20 | public string DefaultSortingField { get; init; } 21 | 22 | [JsonPropertyName("token_separators")] 23 | public IReadOnlyCollection TokenSeparators { get; init; } 24 | 25 | [JsonPropertyName("symbols_to_index")] 26 | public IReadOnlyCollection SymbolsToIndex { get; init; } 27 | 28 | [JsonPropertyName("enable_nested_fields")] 29 | public bool EnableNestedFields { get; init; } 30 | 31 | [JsonPropertyName("metadata")] 32 | public IDictionary? Metadata { get; init; } 33 | 34 | [JsonPropertyName("created_at"), JsonConverter(typeof(UnixEpochDateTimeConverter))] 35 | public DateTime CreatedAt { get; init; } 36 | 37 | [JsonConstructor] 38 | public CollectionResponse( 39 | string name, 40 | int numberOfDocuments, 41 | IReadOnlyCollection fields, 42 | string defaultSortingField, 43 | IReadOnlyCollection tokenSeparators, 44 | IReadOnlyCollection symbolsToIndex, 45 | bool enableNestedFields, 46 | IDictionary? metadata = null) 47 | { 48 | Name = name; 49 | NumberOfDocuments = numberOfDocuments; 50 | Fields = fields; 51 | DefaultSortingField = defaultSortingField; 52 | TokenSeparators = tokenSeparators; 53 | SymbolsToIndex = symbolsToIndex; 54 | EnableNestedFields = enableNestedFields; 55 | Metadata = metadata; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Typesense/Converter/JsonStringEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | 8 | namespace Typesense.Converter; 9 | 10 | public class JsonStringEnumConverter : JsonConverter where TEnum : struct, Enum 11 | { 12 | // These can be made FrozenDictionary when .NET 6 & 7 support is removed 13 | private static readonly Dictionary EnumToString; 14 | private static readonly Dictionary StringToEnum = new(); 15 | 16 | // Disabled CA1810 so that EnumToString can be set to the correct capacity. 17 | // Done outside of the static constructor won't result in any benefits. 18 | #pragma warning disable CA1810 19 | static JsonStringEnumConverter() 20 | { 21 | var type = typeof(TEnum); 22 | var values = Enum.GetValues(); 23 | EnumToString = new Dictionary(capacity: values.Length); 24 | 25 | foreach (var value in values) 26 | { 27 | var stringValue = value.ToString(); 28 | var enumMember = type.GetMember(stringValue)[0]; 29 | var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false) 30 | .Cast() 31 | .FirstOrDefault(); 32 | 33 | StringToEnum.Add(stringValue, value); 34 | 35 | if (attr?.Value != null) 36 | { 37 | EnumToString.Add(value, attr.Value); 38 | StringToEnum.Add(attr.Value, value); 39 | } 40 | else 41 | { 42 | EnumToString.Add(value, stringValue); 43 | } 44 | } 45 | } 46 | #pragma warning restore CA1810 47 | 48 | public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 49 | { 50 | var stringValue = reader.GetString(); 51 | if (stringValue is null) 52 | throw new InvalidOperationException($"Received null value from {nameof(reader)}."); 53 | 54 | return StringToEnum.GetValueOrDefault(stringValue); 55 | } 56 | 57 | public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) 58 | { 59 | ArgumentNullException.ThrowIfNull(writer); 60 | 61 | writer.WriteStringValue(EnumToString[value]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/Typesense.Tests/TypesenseConverterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using FluentAssertions.Execution; 4 | using System.Collections.Generic; 5 | using System.Text.Json; 6 | using Xunit; 7 | 8 | namespace Typesense.Tests; 9 | 10 | [Trait("Category", "Unit")] 11 | public class TypesenseConverterTests 12 | { 13 | [Fact] 14 | public void TestMixedTokens() 15 | { 16 | var json = @" 17 | [ 18 | { 19 | ""field"": ""tags"", 20 | ""indices"": [ 21 | 0 22 | ], 23 | ""matched_tokens"": [ 24 | [ 25 | ""Bar"" 26 | ] 27 | ], 28 | ""snippets"": [ 29 | ""Bar"" 30 | ] 31 | }, 32 | { 33 | ""field"": ""title"", 34 | ""matched_tokens"": [ 35 | ""Bar"" 36 | ], 37 | ""snippet"": ""Wayward Star Bar"" 38 | } 39 | ] 40 | "; 41 | 42 | var response = JsonSerializer.Deserialize(json); 43 | 44 | using (new AssertionScope()) 45 | { 46 | response.Should().NotBeNull(); 47 | response.Length.Should().Be(2); 48 | response[0].MatchedTokens.Count.Should().Be(1); 49 | response[0].MatchedTokens[0].Should().BeOfType>(); 50 | response[1].MatchedTokens.Count.Should().Be(1); 51 | response[1].MatchedTokens[0].Should().BeOfType(); 52 | } 53 | } 54 | 55 | [Fact] 56 | public void TestGroupKeys() 57 | { 58 | var json = @" 59 | { 60 | ""found"": 36, 61 | ""group_key"": [""USA"", 12, 4.2, [""foo"", ""bar""], [42, 4.5]], 62 | ""Hits"": [] 63 | } 64 | "; 65 | 66 | var groupedHit = JsonSerializer.Deserialize>(json); 67 | 68 | using (new AssertionScope()) 69 | { 70 | groupedHit.Should().NotBeNull(); 71 | groupedHit.Found.Should().Be(36); 72 | 73 | var key = groupedHit.GroupKey; 74 | key.Count.Should().Be(5); 75 | 76 | key[0].Should().Be("USA"); 77 | key[1].Should().Be("12"); 78 | key[2].Should().Be("4.2"); 79 | key[3].Should().Be("foo, bar"); 80 | key[4].Should().Be("42, 4.5"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Typesense/SearchOverrideResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | using Typesense.Converter; 5 | 6 | namespace Typesense; 7 | 8 | public record SearchOverrideResponse 9 | { 10 | [JsonPropertyName("id")] 11 | public string Id { get; init; } 12 | 13 | [JsonPropertyName("excludes")] 14 | public IEnumerable Excludes { get; init; } 15 | 16 | [JsonPropertyName("includes")] 17 | public IEnumerable Includes { get; init; } 18 | 19 | [JsonPropertyName("metadata")] 20 | public IDictionary Metadata { get; init; } 21 | 22 | [JsonPropertyName("filter_by")] 23 | public string FilterBy { get; init; } 24 | 25 | [JsonPropertyName("sort_by")] 26 | public string SortBy { get; init; } 27 | 28 | [JsonPropertyName("replace_query")] 29 | public string ReplaceQuery { get; init; } 30 | 31 | [JsonPropertyName("remove_matched_tokens")] 32 | public bool RemoveMatchedTokens { get; init; } 33 | 34 | [JsonPropertyName("filter_curated_hits")] 35 | public bool FilterCuratedHits { get; init; } 36 | 37 | [JsonPropertyName("stop_processing")] 38 | public bool StopProcessing { get; init; } 39 | 40 | [JsonPropertyName("effective_from_ts"), JsonConverter(typeof(UnixEpochDateTimeLongConverter))] 41 | public DateTime? EffectiveFromTs { get; init; } 42 | 43 | [JsonPropertyName("effective_to_ts"), JsonConverter(typeof(UnixEpochDateTimeLongConverter))] 44 | public DateTime? EffectiveToTs { get; init; } 45 | 46 | [JsonPropertyName("rule")] 47 | public Rule Rule { get; init; } 48 | 49 | [JsonConstructor] 50 | public SearchOverrideResponse(IEnumerable excludes, IEnumerable includes, 51 | IDictionary metadata, string filterBy, string sortBy, string replaceQuery, bool removeMatchedTokens, 52 | bool filterCuratedHits, bool stopProcessing, DateTime? effectiveFromTs, DateTime? effectiveToTs, 53 | Rule rule, string id) 54 | { 55 | Excludes = excludes; 56 | Includes = includes; 57 | Metadata = metadata; 58 | FilterBy = filterBy; 59 | SortBy = sortBy; 60 | ReplaceQuery = replaceQuery; 61 | RemoveMatchedTokens = removeMatchedTokens; 62 | FilterCuratedHits = filterCuratedHits; 63 | StopProcessing = stopProcessing; 64 | EffectiveFromTs = effectiveFromTs; 65 | EffectiveToTs = effectiveToTs; 66 | Rule = rule; 67 | Id = id; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | dotnet-core-sdk: 5 | docker: 6 | - image: mcr.microsoft.com/dotnet/sdk:8.0 7 | auth: 8 | username: $DOCKER_LOGIN 9 | password: $DOCKER_ACCESSTOKEN 10 | dotnet-core-sdk-testing: 11 | docker: 12 | - image: mcr.microsoft.com/dotnet/sdk:8.0 13 | auth: 14 | username: $DOCKER_LOGIN 15 | password: $DOCKER_ACCESSTOKEN 16 | # For integration testing 17 | - image: typesense/typesense:28.0 18 | auth: 19 | username: $DOCKER_LOGIN 20 | password: $DOCKER_ACCESSTOKEN 21 | command: [--data-dir=/tmp, --api-key=key] 22 | 23 | jobs: 24 | build-app: 25 | executor: dotnet-core-sdk 26 | steps: 27 | - checkout 28 | - run: 29 | name: Build 30 | command: dotnet build 31 | 32 | test-app: 33 | executor: dotnet-core-sdk-testing 34 | steps: 35 | - checkout 36 | - run: 37 | name: install dockerize 38 | command: | 39 | wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz 40 | tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz 41 | rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz 42 | environment: 43 | DOCKERIZE_VERSION: v0.3.0 44 | - run: 45 | name: Wait for Typesense 46 | command: dockerize -wait tcp://localhost:8108 -timeout 1m 47 | - run: 48 | name: Test 49 | command: dotnet test 50 | 51 | publish-nuget: 52 | executor: dotnet-core-sdk 53 | steps: 54 | - checkout 55 | - run: 56 | name: Push to NuGet 57 | command: | 58 | cd src/Typesense 59 | dotnet pack -o ./publish --no-dependencies -c Release -p:PackageVersion=${CIRCLE_TAG} 60 | dotnet nuget push --source "${NUGET_FEED_URL}" --api-key="${NUGET_KEY}" "./publish/*.nupkg" 61 | 62 | workflows: 63 | build-test-publish_nuget: 64 | jobs: 65 | - build-app: 66 | filters: 67 | tags: 68 | only: /.*/ 69 | - test-app: 70 | requires: 71 | - build-app 72 | filters: 73 | tags: 74 | only: /.*/ 75 | - publish-nuget: 76 | context: nuget 77 | requires: 78 | - test-app 79 | filters: 80 | tags: 81 | only: /^[0-9].*/ 82 | branches: 83 | ignore: /.*/ 84 | -------------------------------------------------------------------------------- /src/Typesense/StatsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public sealed record StatsResponse 7 | { 8 | [JsonPropertyName("delete_latency_ms")] 9 | public decimal DeleteLatencyMs { get; init; } 10 | 11 | [JsonPropertyName("delete_requests_per_second")] 12 | public decimal DeleteRequestsPerSecond { get; init; } 13 | 14 | [JsonPropertyName("import_latency_ms")] 15 | public decimal ImportLatencyMs { get; init; } 16 | 17 | [JsonPropertyName("import_requests_per_second")] 18 | public decimal ImportRequestsPerSecond { get; init; } 19 | 20 | [JsonPropertyName("latency_ms")] 21 | public Dictionary LatencyMs { get; init; } 22 | 23 | [JsonPropertyName("pending_write_batches")] 24 | public decimal PendingWriteBatches { get; init; } 25 | 26 | [JsonPropertyName("requests_per_second")] 27 | public Dictionary RequestsPerSecond { get; init; } 28 | 29 | [JsonPropertyName("search_latency_ms")] 30 | public decimal SearchLatencyMs { get; init; } 31 | 32 | [JsonPropertyName("search_requests_per_second")] 33 | public decimal SearchRequestsPerSecond { get; init; } 34 | 35 | [JsonPropertyName("total_requests_per_second")] 36 | public decimal TotalRequestsPerSecond { get; init; } 37 | 38 | [JsonPropertyName("write_latency_ms")] 39 | public decimal WriteLatencyMs { get; init; } 40 | 41 | [JsonPropertyName("write_requests_per_second")] 42 | public decimal WriteRequestsPerSecond { get; init; } 43 | 44 | [JsonConstructor] 45 | public StatsResponse( 46 | decimal deleteLatencyMs, 47 | decimal deleteRequestsPerSecond, 48 | decimal importLatencyMs, 49 | decimal importRequestsPerSecond, 50 | Dictionary latencyMs, 51 | decimal pendingWriteBatches, 52 | Dictionary requestsPerSecond, 53 | decimal searchLatencyMs, 54 | decimal searchRequestsPerSecond, 55 | decimal totalRequestsPerSecond, 56 | decimal writeLatencyMs, 57 | decimal writeRequestsPerSecond 58 | ) 59 | { 60 | DeleteLatencyMs = deleteLatencyMs; 61 | DeleteRequestsPerSecond = deleteRequestsPerSecond; 62 | ImportLatencyMs = importLatencyMs; 63 | ImportRequestsPerSecond = importRequestsPerSecond; 64 | LatencyMs = latencyMs; 65 | PendingWriteBatches = pendingWriteBatches; 66 | RequestsPerSecond = requestsPerSecond; 67 | SearchLatencyMs = searchLatencyMs; 68 | SearchRequestsPerSecond = searchRequestsPerSecond; 69 | TotalRequestsPerSecond = totalRequestsPerSecond; 70 | WriteLatencyMs = writeLatencyMs; 71 | WriteRequestsPerSecond = writeRequestsPerSecond; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Typesense/UpdateSchema.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Typesense; 5 | 6 | public record UpdateSchemaField : Field 7 | { 8 | public bool? Drop { get; init; } 9 | 10 | public UpdateSchemaField(string name, bool drop) : base(name) 11 | { 12 | Drop = drop; 13 | } 14 | 15 | public UpdateSchemaField( 16 | string name, 17 | FieldType type) : base(name, type) { } 18 | 19 | public UpdateSchemaField( 20 | string name, 21 | FieldType type, 22 | bool? facet = null) : base(name, type, facet) { } 23 | 24 | public UpdateSchemaField( 25 | string name, 26 | FieldType type, 27 | bool? facet = null, 28 | bool? optional = null) : base(name, type, facet, optional) { } 29 | 30 | public UpdateSchemaField( 31 | string name, 32 | FieldType type, 33 | bool? facet = null, 34 | bool? optional = null, 35 | bool? index = null) : base(name, type, facet, optional, index) { } 36 | 37 | public UpdateSchemaField( 38 | string name, 39 | FieldType type, 40 | bool? facet = null, 41 | bool? optional = null, 42 | bool? index = null, 43 | bool? sort = null) : base(name, type, facet, optional, index, sort) { } 44 | 45 | public UpdateSchemaField( 46 | string name, 47 | FieldType type, 48 | bool? facet = null, 49 | bool? optional = null, 50 | bool? index = null, 51 | bool? sort = null, 52 | bool? infix = null) : base(name, type, facet, optional, index, sort, infix) { } 53 | 54 | public UpdateSchemaField( 55 | string name, 56 | FieldType type, 57 | bool? facet = null, 58 | bool? optional = null, 59 | bool? index = null, 60 | bool? sort = null, 61 | bool? infix = null, 62 | string? locale = null) : base(name, type, facet, optional, index, sort, infix, locale) { } 63 | 64 | [JsonConstructor] 65 | public UpdateSchemaField( 66 | string name, 67 | FieldType type, 68 | bool? facet = null, 69 | bool? optional = null, 70 | bool? index = null, 71 | bool? sort = null, 72 | bool? infix = null, 73 | string? locale = null, 74 | bool? drop = null) : base(name, type, facet, optional, index, sort, infix, locale) 75 | { 76 | Drop = drop; 77 | } 78 | 79 | protected UpdateSchemaField(Field original) : base(original) { } 80 | } 81 | 82 | public record UpdateSchema 83 | { 84 | [JsonPropertyName("fields")] 85 | public IReadOnlyCollection Fields { get; init; } 86 | 87 | [JsonConstructor] 88 | public UpdateSchema(IReadOnlyCollection fields) 89 | { 90 | Fields = fields; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Typesense/Setup/Node.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Typesense.Setup; 4 | /// 5 | /// The node contains the configuration for a remote Typesense service. 6 | /// 7 | public record Node 8 | { 9 | /// 10 | /// Hostname for the Typesense service. 11 | /// 12 | public string Host { get; set; } 13 | /// 14 | /// Port for the Typesense service. 15 | /// 16 | public string Port { get; set; } 17 | /// 18 | /// Protocol for the Typesense service - defaults to http. 19 | /// 20 | public string Protocol { get; set; } = "http"; 21 | /// 22 | /// Protocol for the Typesense service - defaults to http. 23 | /// 24 | public string AdditionalPath { get; set; } = ""; 25 | 26 | [Obsolete("Use multi-arity constructor instead.")] 27 | public Node() 28 | { 29 | Host = ""; 30 | Port = ""; 31 | Protocol = ""; 32 | } 33 | 34 | /// Hostname for the Typesense service. 35 | /// Port for the typesense service. 36 | /// Protocol for the Typesense service - defaults to http. 37 | /// 38 | public Node(string host, string port, string protocol = "http") 39 | { 40 | if (string.IsNullOrEmpty(host)) 41 | { 42 | throw new ArgumentException("Cannot be NULL or empty.", nameof(host)); 43 | } 44 | 45 | if (string.IsNullOrEmpty(protocol)) 46 | { 47 | throw new ArgumentException("Cannot be NULL or empty.", nameof(protocol)); 48 | } 49 | 50 | if (string.IsNullOrEmpty(port)) 51 | { 52 | throw new ArgumentException("Cannot be NULL or empty.", nameof(port)); 53 | } 54 | 55 | Host = host; 56 | Port = port; 57 | Protocol = protocol; 58 | } 59 | 60 | /// Hostname for the Typesense service. 61 | /// Port for the typesense service. 62 | /// Protocol for the Typesense service - defaults to http. 63 | /// additionalPath for the Typesense service - defaults to empty string. 64 | /// 65 | public Node(string host, string port, string protocol = "http", string additionalPath = "") 66 | { 67 | if (string.IsNullOrEmpty(host)) 68 | { 69 | throw new ArgumentException("Cannot be NULL or empty.", nameof(host)); 70 | } 71 | 72 | if (string.IsNullOrEmpty(protocol)) 73 | { 74 | throw new ArgumentException("Cannot be NULL or empty.", nameof(protocol)); 75 | } 76 | 77 | if (string.IsNullOrEmpty(port)) 78 | { 79 | throw new ArgumentException("Cannot be NULL or empty.", nameof(port)); 80 | } 81 | 82 | Host = host; 83 | Port = port; 84 | Protocol = protocol; 85 | AdditionalPath = additionalPath; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Typesense/TypesenseApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Typesense; 4 | 5 | /// 6 | /// TypesenseApiException general TypesenseApiException. 7 | /// 8 | public class TypesenseApiException : Exception 9 | { 10 | public TypesenseApiException() { } 11 | public TypesenseApiException(string message) : base(message) { } 12 | public TypesenseApiException(string message, Exception innerException) : base(message, innerException) { } 13 | } 14 | 15 | /// 16 | /// Bad Request - The request could not be understood due to malformed syntax. 17 | /// 18 | public class TypesenseApiBadRequestException : TypesenseApiException 19 | { 20 | public TypesenseApiBadRequestException() { } 21 | public TypesenseApiBadRequestException(string message) : base(message) { } 22 | public TypesenseApiBadRequestException(string message, Exception innerException) : base(message, innerException) { } 23 | } 24 | 25 | /// 26 | /// Unauthorized - Your API key is wrong. 27 | /// 28 | public class TypesenseApiUnauthorizedException : TypesenseApiException 29 | { 30 | public TypesenseApiUnauthorizedException() { } 31 | public TypesenseApiUnauthorizedException(string message) : base(message) { } 32 | public TypesenseApiUnauthorizedException(string message, Exception innerException) : base(message, innerException) { } 33 | } 34 | 35 | /// 36 | /// Not Found - The requested resource is not found. 37 | /// 38 | public class TypesenseApiNotFoundException : TypesenseApiException 39 | { 40 | public TypesenseApiNotFoundException() { } 41 | public TypesenseApiNotFoundException(string message) : base(message) { } 42 | public TypesenseApiNotFoundException(string message, Exception innerException) : base(message, innerException) { } 43 | } 44 | 45 | /// 46 | /// Conflict - When a resource already exists. 47 | /// 48 | public class TypesenseApiConflictException : TypesenseApiException 49 | { 50 | public TypesenseApiConflictException() { } 51 | public TypesenseApiConflictException(string message) : base(message) { } 52 | public TypesenseApiConflictException(string message, Exception innerException) : base(message, innerException) { } 53 | } 54 | 55 | /// 56 | /// Unprocessable Entity - Request is well-formed, but cannot be processed. 57 | /// 58 | public class TypesenseApiUnprocessableEntityException : TypesenseApiException 59 | { 60 | public TypesenseApiUnprocessableEntityException() { } 61 | public TypesenseApiUnprocessableEntityException(string message) : base(message) { } 62 | public TypesenseApiUnprocessableEntityException(string message, Exception innerException) : base(message, innerException) { } 63 | } 64 | 65 | /// 66 | /// Service Unavailable - We’re temporarily offline. Please try again later. 67 | /// 68 | public class TypesenseApiServiceUnavailableException : TypesenseApiException 69 | { 70 | public TypesenseApiServiceUnavailableException() { } 71 | public TypesenseApiServiceUnavailableException(string message) : base(message) { } 72 | public TypesenseApiServiceUnavailableException(string message, Exception innerException) : base(message, innerException) { } 73 | } 74 | -------------------------------------------------------------------------------- /src/Typesense/HttpContents/StreamStringLinesHttpContent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Typesense.HttpContents; 9 | 10 | public sealed class StreamStringLinesHttpContent : System.Net.Http.HttpContent 11 | { 12 | private readonly IEnumerable _lines; 13 | 14 | public StreamStringLinesHttpContent(IEnumerable lines) 15 | { 16 | _lines = lines ?? throw new ArgumentNullException(nameof(lines)); 17 | } 18 | protected override bool TryComputeLength(out long length) 19 | { 20 | // This content doesn't support pre-computed length and 21 | // the request will NOT contain Content-Length header. 22 | // It defeats the purpose of streaming the lines 23 | length = 0; 24 | return false; 25 | } 26 | 27 | // SerializeToStream* methods are internally used by CopyTo* methods 28 | // which in turn are used to copy the content to the NetworkStream. 29 | protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) 30 | { 31 | return SerializeToStreamAsync(stream, context, cancellationToken: CancellationToken.None); 32 | } 33 | 34 | // Override SerializeToStreamAsync overload with CancellationToken 35 | // if the content serialization supports cancellation, otherwise the token will be dropped. 36 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) 37 | { 38 | await using StreamWriter streamWriter = new(stream, leaveOpen: true); 39 | foreach (var line in _lines) 40 | await streamWriter.WriteLineAsync(line).ConfigureAwait(false); 41 | } 42 | 43 | // In rare cases when synchronous support is needed, e.g. synchronous CopyTo used by HttpClient.Send, 44 | // implement synchronous version of SerializeToStream. 45 | protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) 46 | { 47 | using StreamWriter streamWriter = new(stream, leaveOpen: true); 48 | foreach (var line in _lines) 49 | streamWriter.WriteLine(line); 50 | } 51 | 52 | // CreateContentReadStream* methods, if implemented, will be used by ReadAsStream* methods 53 | // to get the underlying stream and avoid buffering. 54 | // These methods will not be used by HttpClient on a custom content. 55 | // They are for content receiving and HttpClient uses its own internal implementation for an HTTP response content. 56 | protected override Task CreateContentReadStreamAsync() 57 | { 58 | return CreateContentReadStreamAsync(cancellationToken: CancellationToken.None); 59 | } 60 | 61 | // Override CreateContentReadStreamAsync overload with CancellationToken 62 | // if the content serialization supports cancellation, otherwise the token will be dropped. 63 | protected override Task CreateContentReadStreamAsync(CancellationToken cancellationToken) 64 | { 65 | return Task.FromResult(CreateContentReadStream(cancellationToken)); 66 | } 67 | 68 | // In rare cases when synchronous support is needed, e.g. synchronous ReadAsStream, 69 | // implement synchronous version of CreateContentRead. 70 | protected override Stream CreateContentReadStream(CancellationToken cancellationToken) 71 | { 72 | MemoryStream stream = new(); 73 | SerializeToStream(stream, context: null, cancellationToken); 74 | return stream; 75 | } 76 | } -------------------------------------------------------------------------------- /src/Typesense/SearchOverride.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | using Typesense.Converter; 5 | 6 | namespace Typesense; 7 | 8 | public record Exclude 9 | { 10 | [JsonPropertyName("id")] 11 | public string Id { get; init; } 12 | 13 | [JsonConstructor] 14 | public Exclude(string id) 15 | { 16 | if (string.IsNullOrWhiteSpace(id)) 17 | throw new ArgumentException("cannot be null or whitespace.", nameof(id)); 18 | Id = id; 19 | } 20 | } 21 | 22 | public record Include 23 | { 24 | [JsonPropertyName("id")] 25 | public string Id { get; init; } 26 | 27 | [JsonPropertyName("position")] 28 | public int Position { get; init; } 29 | 30 | [JsonConstructor] 31 | public Include(string id, int position) 32 | { 33 | if (string.IsNullOrWhiteSpace(id)) 34 | throw new ArgumentException("cannot be null or whitespace.", nameof(id)); 35 | Id = id; 36 | Position = position; 37 | } 38 | } 39 | 40 | public record Rule 41 | { 42 | [JsonPropertyName("query")] 43 | public string? Query { get; init; } 44 | 45 | [JsonPropertyName("match")] 46 | public string? Match { get; init; } 47 | 48 | [JsonPropertyName("filter_by")] 49 | public string? FilterBy { get; init; } 50 | 51 | [JsonPropertyName("tags")] 52 | public IEnumerable? Tags { get; init; } 53 | 54 | [JsonConstructor] 55 | public Rule( 56 | string? query = null, 57 | string? match = null, 58 | string? filterBy = null, 59 | IEnumerable? tags = null) 60 | { 61 | Match = match; 62 | Query = query; 63 | FilterBy = filterBy; 64 | Tags = tags; 65 | } 66 | } 67 | 68 | public record SearchOverride 69 | { 70 | [JsonPropertyName("excludes")] 71 | public IEnumerable? Excludes { get; init; } 72 | 73 | [JsonPropertyName("includes")] 74 | public IEnumerable? Includes { get; init; } 75 | 76 | /// 77 | /// Custom metadata for the search override. 78 | /// 79 | /// 80 | /// Example metadata JSON: 81 | /// 82 | /// { 83 | /// "metadata": { 84 | /// "createdBy": "admin", 85 | /// "tags": ["featured", "promotion"] 86 | /// } 87 | /// } 88 | /// 89 | /// 90 | [JsonPropertyName("metadata")] 91 | public IDictionary? Metadata { get; init; } 92 | 93 | [JsonPropertyName("filter_by")] 94 | public string? FilterBy { get; init; } 95 | 96 | [JsonPropertyName("sort_by")] 97 | public string? SortBy { get; init; } 98 | 99 | [JsonPropertyName("replace_query")] 100 | public string? ReplaceQuery { get; init; } 101 | 102 | [JsonPropertyName("remove_matched_tokens")] 103 | public bool? RemoveMatchedTokens { get; init; } 104 | 105 | [JsonPropertyName("filter_curated_hits")] 106 | public bool? FilterCuratedHits { get; init; } 107 | 108 | [JsonPropertyName("stop_processing")] 109 | public bool? StopProcessing { get; init; } 110 | 111 | [JsonPropertyName("effective_from_ts"), JsonConverter(typeof(UnixEpochDateTimeLongConverter))] 112 | public DateTime? EffectiveFromTs { get; init; } 113 | 114 | [JsonPropertyName("effective_to_ts"), JsonConverter(typeof(UnixEpochDateTimeLongConverter))] 115 | public DateTime? EffectiveToTs { get; init; } 116 | 117 | [JsonPropertyName("rule")] 118 | public Rule Rule { get; init; } 119 | 120 | public SearchOverride(Rule rule) 121 | { 122 | ArgumentNullException.ThrowIfNull(rule); 123 | Rule = rule; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Typesense/MetricsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Typesense; 4 | 5 | public sealed record MetricsResponse 6 | { 7 | [JsonPropertyName("system_cpu1_active_percentage")] 8 | public string SystemCPU1ActivePercentage { get; init; } 9 | 10 | [JsonPropertyName("system_cpu2_active_percentage")] 11 | public string SystemCPU2ActivePercentage { get; init; } 12 | 13 | [JsonPropertyName("system_cpu3_active_percentage")] 14 | public string SystemCPU3ActivePercentage { get; init; } 15 | 16 | [JsonPropertyName("system_cpu4_active_percentage")] 17 | public string SystemCPU4ActivePercentage { get; init; } 18 | 19 | [JsonPropertyName("system_cpu_active_percentage")] 20 | public string SystemCPUActivePercentage { get; init; } 21 | 22 | [JsonPropertyName("system_disk_total_bytes")] 23 | public string SystemDiskTotalBytes { get; init; } 24 | 25 | [JsonPropertyName("system_disk_used_bytes")] 26 | public string SystemDiskUsedBytes { get; init; } 27 | 28 | [JsonPropertyName("system_memory_total_bytes")] 29 | public string SystemMemoryTotalBytes { get; init; } 30 | 31 | [JsonPropertyName("system_network_received_bytes")] 32 | public string SystemNetworkReceivedBytes { get; init; } 33 | 34 | [JsonPropertyName("system_network_sent_bytes")] 35 | public string SystemNetworkSentBytes { get; init; } 36 | 37 | [JsonPropertyName("typesense_memory_active_bytes")] 38 | public string TypesenseMemoryActiveBytes { get; init; } 39 | 40 | [JsonPropertyName("typesense_memory_allocated_bytes")] 41 | public string TypesenseMemoryAllocatedbytes { get; init; } 42 | 43 | [JsonPropertyName("typesense_memory_fragmentation_ratio")] 44 | public string TypesenseMemoryFragmentationRatio { get; init; } 45 | 46 | [JsonPropertyName("typesense_memory_mapped_bytes")] 47 | public string TypesenseMemoryMappedBytes { get; init; } 48 | 49 | [JsonPropertyName("typesense_memory_metadata_bytes")] 50 | public string TypesenseMemoryMetadataBytes { get; init; } 51 | 52 | [JsonPropertyName("typesense_memory_resident_bytes")] 53 | public string TypesenseMemoryResidentBytes { get; init; } 54 | 55 | [JsonPropertyName("typesense_memory_retained_bytes")] 56 | public string TypenseMemoryRetainedBytes { get; init; } 57 | 58 | [JsonConstructor] 59 | public MetricsResponse( 60 | string systemCPU1ActivePercentage, 61 | string systemCPU2ActivePercentage, 62 | string systemCPU3ActivePercentage, 63 | string systemCPU4ActivePercentage, 64 | string systemCPUActivePercentage, 65 | string systemDiskTotalBytes, 66 | string systemDiskUsedBytes, 67 | string systemMemoryTotalBytes, 68 | string systemnetworkReceivedBytes, 69 | string systemNetworkSentBytes, 70 | string typesenseMemoryActiveBytes, 71 | string typesenseMemoryAllocatedbytes, 72 | string typesenseMemoryFragmentationRatio, 73 | string typesenseMemoryMappedBytes, 74 | string typesenseMemoryMetadataBytes, 75 | string typesenseMemoryResidentBytes, 76 | string typenseMemoryRetainedBytes) 77 | { 78 | SystemCPU1ActivePercentage = systemCPU1ActivePercentage; 79 | SystemCPU2ActivePercentage = systemCPU2ActivePercentage; 80 | SystemCPU3ActivePercentage = systemCPU3ActivePercentage; 81 | SystemCPU4ActivePercentage = systemCPU4ActivePercentage; 82 | SystemCPUActivePercentage = systemCPUActivePercentage; 83 | SystemDiskTotalBytes = systemDiskTotalBytes; 84 | SystemDiskUsedBytes = systemDiskUsedBytes; 85 | SystemMemoryTotalBytes = systemMemoryTotalBytes; 86 | SystemNetworkReceivedBytes = systemnetworkReceivedBytes; 87 | SystemNetworkSentBytes = systemNetworkSentBytes; 88 | TypesenseMemoryActiveBytes = typesenseMemoryActiveBytes; 89 | TypesenseMemoryAllocatedbytes = typesenseMemoryAllocatedbytes; 90 | TypesenseMemoryFragmentationRatio = typesenseMemoryFragmentationRatio; 91 | TypesenseMemoryMappedBytes = typesenseMemoryMappedBytes; 92 | TypesenseMemoryMetadataBytes = typesenseMemoryMetadataBytes; 93 | TypesenseMemoryResidentBytes = typesenseMemoryResidentBytes; 94 | TypenseMemoryRetainedBytes = typenseMemoryRetainedBytes; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Typesense/HttpContents/StreamJsonLinesHttpContent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Text.Json; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Typesense.HttpContents; 10 | 11 | public sealed class StreamJsonLinesHttpContent : System.Net.Http.HttpContent 12 | { 13 | private readonly IEnumerable _lines; 14 | private readonly JsonSerializerOptions _jsonSerializerOptions; 15 | 16 | public StreamJsonLinesHttpContent(IEnumerable lines, JsonSerializerOptions jsonSerializerOptions) 17 | { 18 | _lines = lines ?? throw new ArgumentNullException(nameof(lines)); 19 | _jsonSerializerOptions = jsonSerializerOptions ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); 20 | } 21 | protected override bool TryComputeLength(out long length) 22 | { 23 | // This content doesn't support pre-computed length and 24 | // the request will NOT contain Content-Length header. 25 | // It defeats the purpose of streaming the lines 26 | length = 0; 27 | return false; 28 | } 29 | 30 | // SerializeToStream* methods are internally used by CopyTo* methods 31 | // which in turn are used to copy the content to the NetworkStream. 32 | protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) 33 | { 34 | return SerializeToStreamAsync(stream, context, cancellationToken: CancellationToken.None); 35 | } 36 | 37 | // Override SerializeToStreamAsync overload with CancellationToken 38 | // if the content serialization supports cancellation, otherwise the token will be dropped. 39 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) 40 | { 41 | ArgumentNullException.ThrowIfNull(stream); 42 | foreach (var line in _lines) 43 | { 44 | await JsonSerializer.SerializeAsync(stream, line, _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); 45 | WriteNewLine(stream); 46 | } 47 | } 48 | 49 | // In rare cases when synchronous support is needed, e.g. synchronous CopyTo used by HttpClient.Send, 50 | // implement synchronous version of SerializeToStream. 51 | protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) 52 | { 53 | ArgumentNullException.ThrowIfNull(stream); 54 | foreach (var line in _lines) 55 | { 56 | JsonSerializer.Serialize(stream, line, _jsonSerializerOptions); 57 | WriteNewLine(stream); 58 | } 59 | } 60 | 61 | // WriteByte allocate a new array with the byte in the Stream default implementation, 62 | // so it's faster to call with with an array of one byte 63 | // https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.writebyte#notes-to-inheritors 64 | private static void WriteNewLine(Stream stream) => stream.Write(Bytes.NewLine); 65 | 66 | // CreateContentReadStream* methods, if implemented, will be used by ReadAsStream* methods 67 | // to get the underlying stream and avoid buffering. 68 | // These methods will not be used by HttpClient on a custom content. 69 | // They are for content receiving and HttpClient uses its own internal implementation for an HTTP response content. 70 | protected override Task CreateContentReadStreamAsync() 71 | { 72 | return CreateContentReadStreamAsync(cancellationToken: CancellationToken.None); 73 | } 74 | 75 | // Override CreateContentReadStreamAsync overload with CancellationToken 76 | // if the content serialization supports cancellation, otherwise the token will be dropped. 77 | protected override Task CreateContentReadStreamAsync(CancellationToken cancellationToken) 78 | { 79 | return Task.FromResult(CreateContentReadStream(cancellationToken)); 80 | } 81 | 82 | // In rare cases when synchronous support is needed, e.g. synchronous ReadAsStream, 83 | // implement synchronous version of CreateContentRead. 84 | protected override Stream CreateContentReadStream(CancellationToken cancellationToken) 85 | { 86 | MemoryStream stream = new(); 87 | SerializeToStream(stream, context: null, cancellationToken); 88 | return stream; 89 | } 90 | } -------------------------------------------------------------------------------- /typesense-dotnet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7D3A349C-0AC4-4B85-A3BA-C6CCA7D7E0F5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Typesense", "src\Typesense\Typesense.csproj", "{28865C8A-8774-4888-B1CC-3477B089D468}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3530FFEA-B56E-41B1-AAEB-614D225AA5B8}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Typesense.Tests", "test\Typesense.Tests\Typesense.Tests.csproj", "{0E36B35F-11C7-4093-B2EE-AB1424885268}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{E6C9DAB2-255B-466E-84D8-E5A1C5F2EF77}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "examples\Example\Example.csproj", "{24DC9B36-DBDA-4638-B6CE-7A782F6C338B}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {28865C8A-8774-4888-B1CC-3477B089D468}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {28865C8A-8774-4888-B1CC-3477B089D468}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {28865C8A-8774-4888-B1CC-3477B089D468}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {28865C8A-8774-4888-B1CC-3477B089D468}.Debug|x64.Build.0 = Debug|Any CPU 35 | {28865C8A-8774-4888-B1CC-3477B089D468}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {28865C8A-8774-4888-B1CC-3477B089D468}.Debug|x86.Build.0 = Debug|Any CPU 37 | {28865C8A-8774-4888-B1CC-3477B089D468}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {28865C8A-8774-4888-B1CC-3477B089D468}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {28865C8A-8774-4888-B1CC-3477B089D468}.Release|x64.ActiveCfg = Release|Any CPU 40 | {28865C8A-8774-4888-B1CC-3477B089D468}.Release|x64.Build.0 = Release|Any CPU 41 | {28865C8A-8774-4888-B1CC-3477B089D468}.Release|x86.ActiveCfg = Release|Any CPU 42 | {28865C8A-8774-4888-B1CC-3477B089D468}.Release|x86.Build.0 = Release|Any CPU 43 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Debug|x64.ActiveCfg = Debug|Any CPU 46 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Debug|x64.Build.0 = Debug|Any CPU 47 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Debug|x86.ActiveCfg = Debug|Any CPU 48 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Debug|x86.Build.0 = Debug|Any CPU 49 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Release|x64.ActiveCfg = Release|Any CPU 52 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Release|x64.Build.0 = Release|Any CPU 53 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Release|x86.ActiveCfg = Release|Any CPU 54 | {0E36B35F-11C7-4093-B2EE-AB1424885268}.Release|x86.Build.0 = Release|Any CPU 55 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Debug|x64.ActiveCfg = Debug|Any CPU 58 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Debug|x64.Build.0 = Debug|Any CPU 59 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Debug|x86.Build.0 = Debug|Any CPU 61 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Release|x64.ActiveCfg = Release|Any CPU 64 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Release|x64.Build.0 = Release|Any CPU 65 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Release|x86.ActiveCfg = Release|Any CPU 66 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B}.Release|x86.Build.0 = Release|Any CPU 67 | EndGlobalSection 68 | GlobalSection(NestedProjects) = preSolution 69 | {28865C8A-8774-4888-B1CC-3477B089D468} = {7D3A349C-0AC4-4B85-A3BA-C6CCA7D7E0F5} 70 | {0E36B35F-11C7-4093-B2EE-AB1424885268} = {3530FFEA-B56E-41B1-AAEB-614D225AA5B8} 71 | {24DC9B36-DBDA-4638-B6CE-7A782F6C338B} = {E6C9DAB2-255B-466E-84D8-E5A1C5F2EF77} 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /src/Typesense/Field.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using System.Collections.Generic; 4 | 5 | namespace Typesense; 6 | 7 | public record Field 8 | { 9 | [JsonPropertyName("name")] 10 | public string Name { get; init; } 11 | 12 | [JsonPropertyName("type")] 13 | [JsonConverter(typeof(Converter.JsonStringEnumConverter))] 14 | public FieldType Type { get; init; } 15 | 16 | [JsonPropertyName("facet")] 17 | public bool? Facet { get; init; } 18 | 19 | [JsonPropertyName("optional")] 20 | public bool? Optional { get; init; } 21 | 22 | [JsonPropertyName("index")] 23 | public bool? Index { get; init; } 24 | 25 | [JsonPropertyName("store")] 26 | public bool? Store { get; init; } 27 | 28 | [JsonPropertyName("sort")] 29 | public bool? Sort { get; init; } 30 | 31 | [JsonPropertyName("infix")] 32 | public bool? Infix { get; init; } 33 | 34 | [JsonPropertyName("locale")] 35 | public string? Locale { get; init; } 36 | 37 | [JsonPropertyName("num_dim")] 38 | public int? NumberOfDimensions { get; init; } 39 | 40 | [JsonPropertyName("embed")] 41 | public AutoEmbeddingConfig? Embed { get; init; } 42 | 43 | [JsonPropertyName("reference")] 44 | public string? Reference { get; init; } 45 | 46 | [JsonPropertyName("async_reference")] 47 | public bool? AsyncReference { get; init; } 48 | 49 | [JsonPropertyName("stem")] 50 | public bool? Stem { get; init; } 51 | 52 | [JsonPropertyName("vec_dist")] 53 | public string? VecDist { get; init; } 54 | 55 | [JsonPropertyName("hnsw_params")] 56 | public HnswParams? HnswParams { get; init; } 57 | 58 | [JsonPropertyName("range_index")] 59 | public bool? RangeIndex { get; init; } 60 | 61 | [JsonPropertyName("token_separators")] 62 | public IEnumerable? TokenSeparators { get; init; } 63 | 64 | [JsonPropertyName("symbols_to_index")] 65 | public IEnumerable? SymbolsToIndex { get; init; } 66 | 67 | // This constructor is made to handle inherited classes. 68 | protected Field(string name) 69 | { 70 | Name = name; 71 | } 72 | 73 | public Field(string name, FieldType type) 74 | { 75 | Name = name; 76 | Type = type; 77 | } 78 | 79 | public Field(string name, FieldType type, int numberOfDimensions) 80 | { 81 | Name = name; 82 | Type = type; 83 | NumberOfDimensions = numberOfDimensions; 84 | } 85 | 86 | public Field(string name, FieldType type, AutoEmbeddingConfig embed) 87 | { 88 | Name = name; 89 | Type = type; 90 | Embed = embed; 91 | } 92 | 93 | public Field(string name, FieldType type, bool? facet = null) 94 | { 95 | Name = name; 96 | Type = type; 97 | Facet = facet; 98 | } 99 | 100 | public Field( 101 | string name, 102 | FieldType type, 103 | bool? facet = null, 104 | bool? optional = null) 105 | { 106 | Name = name; 107 | Type = type; 108 | Facet = facet; 109 | Optional = optional; 110 | } 111 | 112 | public Field( 113 | string name, 114 | FieldType type, 115 | bool? facet = null, 116 | bool? optional = null, 117 | bool? index = null) 118 | { 119 | Name = name; 120 | Type = type; 121 | Facet = facet; 122 | Optional = optional; 123 | Index = index; 124 | } 125 | 126 | public Field( 127 | string name, 128 | FieldType type, 129 | bool? facet = null, 130 | bool? optional = null, 131 | bool? index = null, 132 | bool? sort = null) 133 | { 134 | Name = name; 135 | Type = type; 136 | Facet = facet; 137 | Optional = optional; 138 | Index = index; 139 | Sort = sort; 140 | } 141 | 142 | public Field( 143 | string name, 144 | FieldType type, 145 | bool? facet = null, 146 | bool? optional = null, 147 | bool? index = null, 148 | bool? sort = null, 149 | bool? infix = null) 150 | { 151 | Name = name; 152 | Type = type; 153 | Facet = facet; 154 | Optional = optional; 155 | Index = index; 156 | Sort = sort; 157 | Infix = infix; 158 | } 159 | 160 | [JsonConstructor] 161 | public Field( 162 | string name, 163 | FieldType type, 164 | bool? facet = null, 165 | bool? optional = null, 166 | bool? index = null, 167 | bool? sort = null, 168 | bool? infix = null, 169 | string? locale = null, 170 | int? numberOfDimensions = null, 171 | AutoEmbeddingConfig? embed = null) 172 | { 173 | Name = name; 174 | Type = type; 175 | Facet = facet; 176 | Optional = optional; 177 | Index = index; 178 | Sort = sort; 179 | Infix = infix; 180 | Locale = locale; 181 | NumberOfDimensions = numberOfDimensions; 182 | Embed = embed; 183 | } 184 | 185 | [Obsolete("A better choice going forward is using the constructor with 'FieldType' enum instead.")] 186 | public Field(string name, string type, bool? facet, bool optional = false, bool index = true) 187 | { 188 | Name = name; 189 | Type = MapFieldType(type); 190 | Facet = facet; 191 | Optional = optional; 192 | Index = index; 193 | } 194 | 195 | private static FieldType MapFieldType(string fieldType) => 196 | fieldType switch 197 | { 198 | "string" => FieldType.String, 199 | "int32" => FieldType.Int32, 200 | "int64" => FieldType.Int64, 201 | "float" => FieldType.Float, 202 | "bool" => FieldType.Bool, 203 | "geopoint" => FieldType.GeoPoint, 204 | "string[]" => FieldType.StringArray, 205 | "int32[]" => FieldType.Int32Array, 206 | "int64[]" => FieldType.Int64Array, 207 | "float[]" => FieldType.FloatArray, 208 | "bool[]" => FieldType.BoolArray, 209 | "geopoint[]" => FieldType.GeoPointArray, 210 | "auto" => FieldType.Auto, 211 | "string*" => FieldType.AutoString, 212 | "image" => FieldType.Image, 213 | _ => throw new ArgumentException($"Could not map field type with value '{fieldType}'", nameof(fieldType)) 214 | }; 215 | } 216 | 217 | public record HnswParams 218 | { 219 | [JsonPropertyName("M")] 220 | public int M { get; init; } 221 | 222 | [JsonPropertyName("ef_construction")] 223 | public int EfConstruction { get; init; } 224 | } 225 | -------------------------------------------------------------------------------- /src/Typesense/VectorSearchQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace Typesense; 10 | 11 | /// 12 | /// Encapsulates the vector query function utilized by Typesense. 13 | /// Examples: 14 | /// vec:([0.34, 0.66, 0.12, 0.68], k: 10) 15 | /// vec:([0.34, 0.66, 0.12, 0.68], k: 10, flat_search_cutoff: 20) 16 | /// vec:([], id: abcd) 17 | /// 18 | public record VectorQuery 19 | { 20 | private float[] _vector = Array.Empty(); 21 | 22 | /// 23 | /// Document vector. 24 | /// 25 | // We only expose `ReadOnlyCollection` to make sure the consumer cannot modify the underlying value. 26 | public ReadOnlyCollection Vector => new(_vector); 27 | 28 | /// 29 | /// Vector field name. 30 | /// 31 | public string VectorFieldName { get; private set; } 32 | 33 | /// 34 | /// Document Id. 35 | /// 36 | public string? Id { get; private set; } 37 | 38 | /// 39 | /// Number of documents to return. 40 | /// 41 | public int? K { get; private set; } 42 | 43 | /// 44 | /// Maximum vector distance threshold for results of semantic search and hybrid search 45 | /// 46 | public decimal? DistanceThreshold { get; private set; } 47 | 48 | /// 49 | /// Allows bypass of the HNSW index to do a flat / brute-force ranking of vectors 50 | /// 51 | public int? FlatSearchCutoff { get; private set; } 52 | 53 | /// 54 | /// Extra parameters to include in the query. 55 | /// 56 | public Dictionary ExtraParams { get; private set; } 57 | 58 | /// 59 | /// Initialize VectorQuery using a raw query. 60 | /// 61 | /// 62 | public VectorQuery(string query) 63 | { 64 | ExtraParams = new(); 65 | VectorFieldName = ""; 66 | ParseQuery(query); 67 | } 68 | 69 | /// 70 | /// Initialize VectorQuery. 71 | /// 72 | /// Document vector 73 | /// Vector field name to be searched against 74 | /// String document id 75 | /// Number of documents that are returned 76 | /// If you wish to do brute-force vector search when a given query matches fewer than 20 documents, sending flat_search_cutoff=20 will bypass the HNSW index when the number of results found is less than 20 77 | /// Any extra parameters you wish to include as a key/value dictionary 78 | /// Maximum vector distance threshold for results of semantic search and hybrid search 79 | /// 80 | public VectorQuery(float[] vector, string vectorFieldName, string? id = null, int? k = null, int? flatSearchCutoff = null, Dictionary? extraParams = null, decimal? distanceThreshold = null) 81 | { 82 | ArgumentNullException.ThrowIfNull(vector); 83 | 84 | if (vector.Length > 0 && id != null) 85 | throw new ArgumentException( 86 | "Malformed vector query string: cannot pass both vector query and `id` parameter."); 87 | 88 | if (vector.Length == 0 && id is null) 89 | throw new ArgumentException("When a vector query value is empty, an `id` parameter must be present."); 90 | 91 | if (string.IsNullOrWhiteSpace(vectorFieldName)) 92 | throw new ArgumentException( 93 | "The vector fieldname cannot be null or whitespace.", 94 | nameof(vectorFieldName)); 95 | 96 | _vector = vector; 97 | VectorFieldName = vectorFieldName; 98 | Id = id; 99 | K = k; 100 | DistanceThreshold = distanceThreshold; 101 | FlatSearchCutoff = flatSearchCutoff; 102 | ExtraParams = extraParams ?? new(); 103 | } 104 | 105 | private static readonly Regex VectorQueryStringRegex = new(@"(.+):\((\[.*?\])(\s*,[^)]+)*\)", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); 106 | /// 107 | /// Parses a query and initializes the related object members. 108 | /// 109 | /// 110 | /// 111 | private void ParseQuery(string query) 112 | { 113 | // First parse the portion of the string inside the vec property - "vec:([0.96826, 0.94, 0.39557, 0.306488], k:100, flat_search_cutoff: 20)" 114 | var match = VectorQueryStringRegex.Match(query); 115 | if (!match.Success) 116 | throw new ArgumentException("Malformed vector query string."); 117 | 118 | var vectorFieldNameMatch = match.Groups[1].Value; 119 | if (string.IsNullOrWhiteSpace(vectorFieldNameMatch)) 120 | throw new ArgumentException("Malformed vectory query string: it is missing the vector field name."); 121 | 122 | VectorFieldName = vectorFieldNameMatch; 123 | 124 | // Since the first parameter MUST be the array of floats this is a quick check to see if it is well-formatted 125 | var vectorMatch = match.Groups[2].Value; 126 | vectorMatch = vectorMatch.Substring(1, vectorMatch.Length - 2); 127 | 128 | if (!String.IsNullOrEmpty(vectorMatch)) 129 | { 130 | // Get the float array query portion 131 | _vector = vectorMatch 132 | .Split(',', StringSplitOptions.TrimEntries) 133 | .Select(x => 134 | { 135 | if (!float.TryParse(x, NumberStyles.Float, CultureInfo.InvariantCulture, out float result)) 136 | throw new ArgumentException( 137 | "Malformed vector query string: one of the vector values is not a float."); 138 | 139 | return result; 140 | }).ToArray(); 141 | } 142 | 143 | // Commas are always used as a delimiter inside the list of parameters 144 | var qParams = match.Groups[3].Value 145 | .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); 146 | 147 | foreach (var param in qParams) 148 | { 149 | var kvp = param.Split(':', StringSplitOptions.TrimEntries); 150 | 151 | if (kvp.Length != 2) 152 | throw new ArgumentException($"Malformed vector query string at parameter '{param}'"); 153 | 154 | switch (kvp[0]) 155 | { 156 | case "id": 157 | if (_vector.Length > 0) 158 | throw new ArgumentException( 159 | "Malformed vector query string: cannot pass both vector query and `id` parameter."); 160 | 161 | Id = kvp[1]; 162 | break; 163 | 164 | case "k": 165 | if (!Int32.TryParse(kvp[1], out int k)) 166 | throw new ArgumentException("Malformed vector query string: k value is not an integer"); 167 | 168 | K = k; 169 | break; 170 | 171 | case "flat_search_cutoff": 172 | if (!Int32.TryParse(kvp[1], out int flatSearchCutoff)) 173 | throw new ArgumentException("Malformed vector query string: flat_search_cutoff value is not an integer"); 174 | 175 | FlatSearchCutoff = flatSearchCutoff; 176 | break; 177 | 178 | default: 179 | ExtraParams.Add(kvp[0], kvp[1]); 180 | break; 181 | } 182 | } 183 | 184 | if (_vector.Length == 0 && Id is null) 185 | throw new ArgumentException("When a vector query value is empty, an `id` parameter must be present."); 186 | } 187 | 188 | /// 189 | /// Convert this vector query into a query useable by Typesense. 190 | /// 191 | /// The vector-search query string. 192 | public virtual string ToQuery() 193 | { 194 | StringBuilder queryStringBuilder = new(VectorFieldName); 195 | queryStringBuilder.Append(":(["); 196 | // Float vector is required, even if empty 197 | for (var index = 0; index < _vector.Length; index++) 198 | { 199 | if (index != 0) 200 | queryStringBuilder.Append(','); 201 | queryStringBuilder.Append(_vector[index].ToString("R", CultureInfo.InvariantCulture)); 202 | } 203 | queryStringBuilder.Append(']'); 204 | 205 | // All other parameters are optional 206 | // note that the only delimiter for all type/value pairs is a comma and there is no need to surround string values with quotations 207 | if (Id != null) 208 | queryStringBuilder.Append(",id:").Append(Id); 209 | 210 | if (K != null) 211 | queryStringBuilder.Append(",k:").Append(K); 212 | 213 | if (FlatSearchCutoff != null) 214 | queryStringBuilder.Append(",flat_search_cutoff:").Append(FlatSearchCutoff); 215 | 216 | // Allow for additional parameters if provided 217 | foreach (var (key, value) in ExtraParams) 218 | queryStringBuilder.Append(',').Append(key).Append(':').Append(value); 219 | 220 | queryStringBuilder.Append(')'); 221 | 222 | return queryStringBuilder.ToString(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Typesense/SearchResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json.Serialization; 5 | using Typesense.Converter; 6 | 7 | namespace Typesense; 8 | 9 | public record Highlight 10 | { 11 | [JsonPropertyName("field")] 12 | public string Field { get; init; } 13 | 14 | [JsonPropertyName("snippet")] 15 | public string? Snippet { get; init; } 16 | 17 | [JsonPropertyName("snippets")] 18 | public IReadOnlyList? Snippets { get; init; } 19 | 20 | [JsonPropertyName("indices")] 21 | public IReadOnlyList? Indices { get; init; } 22 | 23 | [JsonPropertyName("value")] 24 | public string? Value { get; init; } 25 | 26 | [System.Diagnostics.CodeAnalysis.SuppressMessage 27 | ("Naming", "CA1721: Property names should not match get methods", 28 | Justification = "Required because of special case regarding matched tokens.")] 29 | [JsonPropertyName("matched_tokens")] 30 | [JsonConverter(typeof(MatchedTokenConverter))] 31 | public IReadOnlyList MatchedTokens { get; init; } 32 | 33 | public IReadOnlyList GetMatchedTokens() => MatchedTokens.Cast().ToList(); 34 | 35 | public Highlight( 36 | string field, 37 | string? snippet, 38 | IReadOnlyList matchedTokens, 39 | IReadOnlyList? snippets, 40 | IReadOnlyList? indices, 41 | string? value) 42 | { 43 | Field = field; 44 | Snippet = snippet; 45 | MatchedTokens = matchedTokens; 46 | Snippets = snippets; 47 | Indices = indices; 48 | Value = value; 49 | } 50 | } 51 | 52 | public record Hit 53 | { 54 | [JsonPropertyName("highlights")] 55 | public IReadOnlyList Highlights { get; init; } 56 | 57 | [JsonPropertyName("document")] 58 | public T Document { get; init; } 59 | 60 | [JsonPropertyName("text_match")] 61 | public long? TextMatch { get; init; } 62 | 63 | [JsonPropertyName("text_match_info")] 64 | public TextMatchInfo? TextMatchInfo { get; init; } 65 | 66 | [JsonPropertyName("vector_distance")] 67 | public double? VectorDistance { get; init; } 68 | 69 | [JsonPropertyName("geo_distance_meters")] 70 | public IReadOnlyDictionary? GeoDistanceMeters { get; init; } 71 | 72 | [JsonPropertyName("hybrid_search_info")] 73 | public HybridSearchInfo? HybridSearchInfo { get; init; } 74 | 75 | [JsonConstructor] 76 | public Hit(IReadOnlyList highlights, T document, long? textMatch, double? vectorDistance, IReadOnlyDictionary? geoDistanceMeters) 77 | { 78 | Highlights = highlights; 79 | Document = document; 80 | TextMatch = textMatch; 81 | VectorDistance = vectorDistance; 82 | GeoDistanceMeters = geoDistanceMeters; 83 | } 84 | } 85 | 86 | public record HybridSearchInfo 87 | { 88 | [JsonPropertyName("rank_fusion_score")] 89 | public double? RankFusionScore { get; set; } 90 | } 91 | 92 | public record TextMatchInfo 93 | { 94 | [JsonPropertyName("best_field_score")] 95 | public string BestFieldScore { get; set; } 96 | 97 | [JsonPropertyName("best_field_weight")] 98 | public int BestFieldWeight { get; set; } 99 | 100 | [JsonPropertyName("fields_matched")] 101 | public int FieldsMatched { get; set; } 102 | 103 | [JsonPropertyName("num_tokens_dropped")] 104 | public ulong NumTokensDropped { get; set; } 105 | 106 | [JsonPropertyName("score")] 107 | public string Score { get; set; } 108 | 109 | [JsonPropertyName("tokens_matched")] 110 | public int TokensMatched { get; set; } 111 | 112 | [JsonPropertyName("typo_prefix_score")] 113 | public int TypoPrefixScore { get; set; } 114 | } 115 | 116 | public record FacetCount 117 | { 118 | [JsonPropertyName("counts")] 119 | public IReadOnlyList Counts { get; init; } 120 | 121 | [JsonPropertyName("field_name")] 122 | public string FieldName { get; init; } 123 | 124 | [JsonPropertyName("stats")] 125 | public FacetStats Stats { get; init; } 126 | 127 | public FacetCount(string fieldName, IReadOnlyList counts, FacetStats stats) 128 | { 129 | FieldName = fieldName; 130 | Counts = counts; 131 | Stats = stats; 132 | } 133 | } 134 | 135 | public record FacetCountHit 136 | { 137 | [JsonPropertyName("count")] 138 | public int Count { get; init; } 139 | 140 | [JsonPropertyName("highlighted")] 141 | public string Highlighted { get; init; } 142 | 143 | [JsonPropertyName("value")] 144 | public string Value { get; init; } 145 | 146 | [JsonPropertyName("parent")] 147 | public Dictionary? Parent { get; init; } 148 | 149 | public FacetCountHit(string value, int count, string highlighted, Dictionary? parent = null) 150 | { 151 | Value = value; 152 | Count = count; 153 | Highlighted = highlighted; 154 | Parent = parent; 155 | } 156 | } 157 | 158 | public record FacetStats 159 | { 160 | [JsonPropertyName("avg")] 161 | public float Average { get; init; } 162 | 163 | [JsonPropertyName("max")] 164 | public float Max { get; init; } 165 | 166 | [JsonPropertyName("min")] 167 | public float Min { get; init; } 168 | 169 | [JsonPropertyName("sum")] 170 | public float Sum { get; init; } 171 | 172 | [JsonPropertyName("total_values")] 173 | public int TotalValues { get; init; } 174 | 175 | [JsonConstructor] 176 | public FacetStats( 177 | float average, 178 | float max, 179 | float min, 180 | float sum, 181 | int totalValues) 182 | { 183 | Average = average; 184 | Max = max; 185 | Min = min; 186 | Sum = sum; 187 | TotalValues = totalValues; 188 | } 189 | } 190 | 191 | public record GroupedHit 192 | { 193 | [JsonPropertyName("group_key")] 194 | [JsonConverter(typeof(GroupKeyConverter))] 195 | public IReadOnlyList GroupKey { get; init; } 196 | 197 | [JsonPropertyName("hits")] 198 | public IReadOnlyList> Hits { get; init; } 199 | 200 | [JsonPropertyName("found")] 201 | public int Found { get; init; } 202 | 203 | [JsonConstructor] 204 | public GroupedHit( 205 | IReadOnlyList groupKey, 206 | IReadOnlyList> hits, 207 | int found) 208 | { 209 | GroupKey = groupKey; 210 | Hits = hits; 211 | Found = found; 212 | } 213 | } 214 | 215 | public abstract record SearchResultBase 216 | { 217 | [JsonPropertyName("facet_counts")] 218 | public IReadOnlyCollection FacetCounts { get; init; } 219 | 220 | [JsonPropertyName("found")] 221 | public int Found { get; init; } 222 | 223 | [JsonPropertyName("found_docs")] 224 | public int? FoundDocs { get; init; } 225 | 226 | [JsonPropertyName("out_of")] 227 | public int OutOf { get; init; } 228 | 229 | [JsonPropertyName("page")] 230 | public int Page { get; init; } 231 | 232 | [JsonPropertyName("search_time_ms")] 233 | public int SearchTimeMs { get; init; } 234 | 235 | [JsonPropertyName("took_ms")] 236 | [Obsolete("Obsolete since version v0.18.0 use SearchTimeMs instead.")] 237 | public int? TookMs { get; init; } 238 | 239 | [JsonConstructor] 240 | protected SearchResultBase( 241 | IReadOnlyCollection facetCounts, 242 | int found, 243 | int outOf, 244 | int page, 245 | int searchTimeMs, 246 | int? tookMs, 247 | int? foundDocs = null) 248 | { 249 | FacetCounts = facetCounts; 250 | Found = found; 251 | OutOf = outOf; 252 | Page = page; 253 | SearchTimeMs = searchTimeMs; 254 | TookMs = tookMs; 255 | FoundDocs = foundDocs; 256 | } 257 | } 258 | 259 | public record SearchResult : SearchResultBase 260 | { 261 | [JsonPropertyName("hits")] 262 | public IReadOnlyList> Hits { get; init; } 263 | 264 | [JsonConstructor] 265 | public SearchResult( 266 | IReadOnlyCollection facetCounts, 267 | int found, 268 | int outOf, 269 | int page, int searchTimeMs, 270 | int? tookMs, 271 | IReadOnlyList> hits) : base(facetCounts, found, outOf, page, searchTimeMs, tookMs) 272 | { 273 | Hits = hits; 274 | } 275 | } 276 | 277 | public record SearchGroupedResult : SearchResultBase 278 | { 279 | [JsonPropertyName("grouped_hits")] 280 | public IReadOnlyList> GroupedHits { get; init; } 281 | 282 | [JsonConstructor] 283 | public SearchGroupedResult( 284 | IReadOnlyCollection facetCounts, 285 | int found, 286 | int outOf, 287 | int page, 288 | int searchTimeMs, 289 | int? tookMs, 290 | IReadOnlyList> groupedHits 291 | ) : base(facetCounts, found, outOf, page, searchTimeMs, tookMs) 292 | { 293 | GroupedHits = groupedHits; 294 | } 295 | } 296 | 297 | public record MultiSearchResult 298 | { 299 | [JsonPropertyName("facet_counts")] 300 | public IReadOnlyList? FacetCounts { get; init; } 301 | 302 | [JsonPropertyName("found")] 303 | public int? Found { get; init; } 304 | 305 | [JsonPropertyName("found_docs")] 306 | public int? FoundDocs { get; init; } 307 | 308 | [JsonPropertyName("hits")] 309 | public IReadOnlyList>? Hits { get; init; } 310 | 311 | [JsonPropertyName("grouped_hits")] 312 | public IReadOnlyList> GroupedHits { get; init; } 313 | 314 | [JsonPropertyName("out_of")] 315 | public int? OutOf { get; init; } 316 | 317 | [JsonPropertyName("page")] 318 | public int? Page { get; init; } 319 | 320 | [JsonPropertyName("search_cutoff")] 321 | public bool? SearchCutoff { get; init; } 322 | 323 | [JsonPropertyName("search_time_ms")] 324 | public int? SearchTimeMs { get; init; } 325 | 326 | [JsonPropertyName("code")] 327 | public int? ErrorCode { get; init; } 328 | 329 | [JsonPropertyName("error")] 330 | public string? ErrorMessage { get; init; } 331 | 332 | [JsonConstructor] 333 | public MultiSearchResult( 334 | IReadOnlyList? facetCounts, 335 | int? found, 336 | IReadOnlyList>? hits, 337 | int? outOf, 338 | int? page, 339 | bool? searchCutoff, 340 | int? searchTimeMs, 341 | int? errorCode, 342 | string? errorMessage) 343 | { 344 | FacetCounts = facetCounts; 345 | Found = found; 346 | Hits = hits; 347 | OutOf = outOf; 348 | Page = page; 349 | SearchCutoff = searchCutoff; 350 | SearchTimeMs = searchTimeMs; 351 | ErrorCode = errorCode; 352 | ErrorMessage = errorMessage; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typesense-dotnet 2 | 3 | .net client for [Typesense.](https://github.com/typesense/typesense) 4 | 5 | You can get the NuGet package [here.](https://www.nuget.org/packages/Typesense/) 6 | 7 | Feel free to make issues or create pull requests if you find any bugs or there are missing features. 8 | 9 | ## Setup 10 | 11 | Setup in service collection so it can be dependency injected. The `AddTypesenseClient` can be found in the `Typesense.Setup` namespace. Remember to change the settings to match your Typesense service. Right now you can specify multiple nodes, but the implementation has not been completed yet, so if you want to use this for multiple nodes, then put a load balancer in front of your services and point the settings to your load balancer. 12 | If you are using externally hosted Typesense, like Typesense Cloud, it is recommended to enable HTTP compression to lower response times and reduce traffic. 13 | 14 | ```c# 15 | var provider = new ServiceCollection() 16 | .AddTypesenseClient(config => 17 | { 18 | config.ApiKey = "mysecretapikey"; 19 | config.Nodes = new List 20 | { 21 | new Node("localhost", "8108", "http") 22 | }; 23 | }, enableHttpCompression: false).BuildServiceProvider(); 24 | ``` 25 | 26 | After that you can get it from the `provider` instance or dependency inject it. 27 | ```c# 28 | var typesenseClient = provider.GetService(); 29 | ``` 30 | 31 | ## Create collection 32 | 33 | When you create the collection, you can specify each field with `name`, `type` and if it should be a `facet`, an `optional` or an `indexed` field. 34 | 35 | ``` c# 36 | var schema = new Schema( 37 | "Addresses", 38 | new List 39 | { 40 | new Field("id", FieldType.Int32, false), 41 | new Field("houseNumber", FieldType.Int32, false), 42 | new Field("accessAddress", FieldType.String, false, true), 43 | new Field("metadataNotes", FieldType.String, false, true, false), 44 | }, 45 | "houseNumber"); 46 | 47 | var createCollectionResult = await typesenseClient.CreateCollection(schema); 48 | ``` 49 | 50 | The example uses `camelCase` by default for field names, but you override this on the document you want to insert. Below is an example using `snake_case`. 51 | 52 | ```C# 53 | public class Address 54 | { 55 | [JsonPropertyName("id")] 56 | public string Id { get; set; } 57 | [JsonPropertyName("house_number")] 58 | public int HouseNumber { get; set; } 59 | [JsonPropertyName("access_address")] 60 | public string AccessAddress { get; set; } 61 | [JsonPropertyName("metadata_notes")] 62 | public string MetadataNotes { get; set; } 63 | } 64 | ``` 65 | 66 | ## Update collection 67 | 68 | Update existing collection. 69 | 70 | ```C# 71 | var updateSchema = new UpdateSchema(new List 72 | { 73 | // Example deleting existing field. 74 | new UpdateSchemaField("metadataNotes", drop: true), 75 | // Example adding a new field. 76 | new UpdateSchemaField("city", FieldType.String, facet: false, optional: true) 77 | }); 78 | 79 | var updateCollectionResponse = await typesenseClient.UpdateCollection("Addresses", updateSchema); 80 | ``` 81 | 82 | ## Index document 83 | 84 | ```c# 85 | var address = new Address 86 | { 87 | Id = 1, 88 | HouseNumber = 2, 89 | AccessAddress = "Smedgade 25B" 90 | }; 91 | 92 | var createDocumentResult = await typesenseClient.CreateDocument
("Addresses", address); 93 | ``` 94 | 95 | ## Upsert document 96 | 97 | ```c# 98 | var address = new Address 99 | { 100 | Id = 1, 101 | HouseNumber = 2, 102 | AccessAddress = "Smedgade 25B" 103 | }; 104 | 105 | var upsertResult = await typesenseClient.UpsertDocument
("Addresses", address); 106 | ``` 107 | 108 | ## Search document in collection 109 | 110 | ```c# 111 | var query = new SearchParameters("Smed", "accessAddress"); 112 | var searchResult = await typesenseClient.Search
("Addresses", query); 113 | ``` 114 | 115 | ## Search grouped 116 | 117 | ```c# 118 | var query = new GroupedSearchParameters("Stark", "company_name", "country"); 119 | var response = await _client.SearchGrouped("companies", query); 120 | ``` 121 | 122 | ## Multi search documents 123 | 124 | Multi search goes from one query to four, if you need more than that please open an issue and I'll implement more. 125 | 126 | Example of using a single query in multi-search. 127 | 128 | ```c# 129 | var query = new MultiSearchParameters("companies", "Stark", "company_name"); 130 | 131 | var response = await _client.MultiSearch(queryOne); 132 | ``` 133 | 134 | 135 | Example of using a two queries in multi-search. 136 | 137 | ```c# 138 | var queryOne = new MultiSearchParameters("companies", "Stark", "company_name"); 139 | var queryTwo = new MultiSearchParameters("employees", "Kenny", "person_name"); 140 | 141 | var (r1, r2) = await _client.MultiSearch(queryOne, queryTwo); 142 | ``` 143 | 144 | Example of using a three queries in multi-search (you get the pattern currently it goes up to four.). 145 | 146 | ```c# 147 | var queryOne = new MultiSearchParameters("companies", "Stark", "company_name"); 148 | var queryTwo = new MultiSearchParameters("employees", "Kenny", "person_name"); 149 | var queryThree = new MultiSearchParameters("companies", "Awesome Corp.", "company_name"); 150 | 151 | var (r1, r2, r3) = await _client.MultiSearch(queryOne, queryTwo, queryThree); 152 | ``` 153 | 154 | ## Vector search 155 | 156 | Typesense supports the ability to add embeddings generated by your Machine Learning models to each document, and then doing a nearest-neighbor search on them. This lets you build features like similarity search, recommendations, semantic search, visual search, etc. Here is an example of how to create a vector schema, a vector document, and perform a multi search using a vector as a query parameter. 157 | 158 | ```c# 159 | const string COLLECTION_NAME = "address_vector_search"; 160 | 161 | var schema = new Schema( 162 | COLLECTION_NAME, 163 | new List 164 | { 165 | new Field("vec", FieldType.FloatArray, 4) 166 | }); 167 | 168 | _ = await _client.CreateCollection(schema); 169 | 170 | await _client.CreateDocument( 171 | COLLECTION_NAME, 172 | new AddressVectorSearch() 173 | { 174 | Id = "0", 175 | Vec = new float[] { 0.04F, 0.234F, 0.113F, 0.001F } 176 | }); 177 | 178 | var query = new MultiSearchParameters(COLLECTION_NAME, "*") 179 | { 180 | // vec:([0.96826, 0.94, 0.39557, 0.306488], k:100) 181 | VectorQuery = new( 182 | vector: new float[] { 0.96826F, 0.94F, 0.39557F, 0.306488F }, 183 | k: 100) 184 | }; 185 | 186 | var response = await _client.MultiSearch(query); 187 | ``` 188 | 189 | ## Retrieve a document on id 190 | 191 | ```c# 192 | var retrievedDocument = await typesenseClient.RetrieveDocument
("Addresses", "1"); 193 | ``` 194 | 195 | ## Update document on id 196 | 197 | ```c# 198 | var address = new Address 199 | { 200 | Id = 1, 201 | HouseNumber = 2, 202 | AccessAddress = "Smedgade 25B" 203 | }; 204 | 205 | var updateDocumentResult = await typesenseClient.UpdateDocument
("Addresses", "1", address); 206 | ``` 207 | 208 | ## Delete document on id 209 | 210 | ```c# 211 | var deleteResult = await typesenseClient.DeleteDocument
("Addresses", "1"); 212 | ``` 213 | 214 | ## Update documents using filter 215 | 216 | ```c# 217 | var updateResult = await typesenseClient.UpdateDocuments("Addresses", "houseNumber:=2", { "accessAddress": "Smedgade 25C" }); 218 | ``` 219 | 220 | ## Delete documents using filter 221 | 222 | ```c# 223 | var deleteResult = await typesenseClient.DeleteDocuments("Addresses", "houseNumber:>=3", 100); 224 | ``` 225 | 226 | ## Drop a collection on name 227 | 228 | ```c# 229 | var deleteCollectionResult = await typesenseClient.DeleteCollection("Addresses"); 230 | ``` 231 | 232 | ## Import documents 233 | 234 | The default batch size is `40`. 235 | The default ImportType is `Create`. 236 | You can pick between three different import types `Create`, `Upsert`, `Update`. 237 | The returned values are a list of `ImportResponse` that contains a `success code`, `error` and the failed `document` as a string representation. 238 | 239 | ```c# 240 | 241 | var importDocumentResults = await typesenseClient.ImportDocuments
("Addresses", addresses, 40, ImportType.Create); 242 | ``` 243 | 244 | ## Export documents 245 | 246 | ```c# 247 | var addresses = await typesenseClient.ExportDocuments
("Addresses"); 248 | ``` 249 | 250 | ## Api keys 251 | 252 | 253 | ### Create key 254 | 255 | `ExpiresAt` is optional. 256 | `Value` is optional. 257 | 258 | ```c# 259 | var apiKey = new Key( 260 | "Example key one", 261 | new[] { "*" }, 262 | new[] { "*" }); 263 | 264 | var createdKey = await typesenseClient.CreateKey(apiKey); 265 | ``` 266 | 267 | ### Retrieve key 268 | 269 | ```c# 270 | var retrievedKey = await typesenseClient.RetrieveKey(0); 271 | ``` 272 | 273 | ### List keys 274 | 275 | ```c# 276 | var keys = await typesenseClient.ListKeys(); 277 | ``` 278 | 279 | ### Delete key 280 | 281 | ```c# 282 | var deletedKey = await typesenseClient.DeleteKey(0); 283 | ``` 284 | 285 | ### Generate Scoped Search key 286 | 287 | ```c# 288 | var scopedSearchKey = typesenseClient.GenerateScopedSearchKey("MainOrParentAPIKey", "{\"filter_by\":\"accessible_to_user_ids:2\"}"); 289 | ``` 290 | 291 | ## Curation 292 | 293 | While Typesense makes it really easy and intuitive to deliver great search results, sometimes you might want to promote certain documents over others. Or, you might want to exclude certain documents from a query's result set. 294 | 295 | Using overrides, you can include or exclude specific documents for a given query. 296 | 297 | 298 | ### Upsert 299 | 300 | ```c# 301 | var searchOverride = new SearchOverride(new List { new Include("2", 1) }, new Rule("Sul", "exact")); 302 | var upsertSearchOverrideResponse = await typesenseClient.UpsertSearchOverride("Addresses", "addresses-override", searchOverride); 303 | ``` 304 | 305 | ### List all overrides 306 | 307 | ```c# 308 | var listSearchOverrides = await typesenseClient.ListSearchOverrides("Addresses"); 309 | ``` 310 | 311 | ### Retrieve overrides 312 | 313 | ```c# 314 | var retrieveSearchOverride = await typesenseClient.RetrieveSearchOverride("Addresses", "addresses-override"); 315 | ``` 316 | 317 | ### Delete override 318 | 319 | ```c# 320 | var deletedSearchOverrideResult = await typesenseClient.DeleteSearchOverride("Addresses", "addresses-override"); 321 | ``` 322 | 323 | ## Collection alias 324 | 325 | An alias is a virtual collection name that points to a real collection. Read more [here.](https://typesense.org/docs/0.21.0/api/collection-alias.html) 326 | 327 | ### Upsert collection alias 328 | 329 | ```c# 330 | var upsertCollectionAlias = await typesenseClient.UpsertCollectionAlias("Address_Alias", new CollectionAlias("Addresses")); 331 | ``` 332 | 333 | ### List all collection aliases 334 | 335 | ```c# 336 | var listCollectionAliases = await typesenseClient.ListCollectionAliases(); 337 | ``` 338 | 339 | ### Retrieve collection alias 340 | 341 | ```c# 342 | var retrieveCollectionAlias = await typesenseClient.RetrieveCollectionAlias("Address_Alias"); 343 | ``` 344 | 345 | ### Delete collection alias 346 | 347 | ```c# 348 | var deleteCollectionAlias = await typesenseClient.DeleteCollectionAlias("Addresses_Alias"); 349 | ``` 350 | 351 | ## Synonyms 352 | 353 | The synonyms feature allows you to define search terms that should be considered equivalent. For eg: when you define a synonym for sneaker as shoe, searching for sneaker will now return all records with the word shoe in them, in addition to records with the word sneaker. Read more [here.](https://typesense.org/docs/0.21.0/api/synonyms.html#synonyms) 354 | 355 | ### Upsert synonym 356 | 357 | ```c# 358 | var upsertSynonym = await typesenseClient.UpsertSynonym("Addresses", "Address_Synonym", new SynonymSchema(new List { "Sultan", "Soltan", "Softan" })); 359 | ``` 360 | 361 | ### Retrieve a synonym 362 | 363 | ```c# 364 | var retrieveSynonym = await typesenseClient.RetrieveSynonym("Addresses", "Address_Synonym"); 365 | ``` 366 | 367 | ### List all synonyms 368 | 369 | ```c# 370 | var listSynonyms = await typesenseClient.ListSynonyms("Addresses"); 371 | ``` 372 | 373 | ### Delete synonym 374 | 375 | ```c# 376 | var deleteSynonym = await typesenseClient.DeleteSynonym("Addresses", "Address_Synonym"); 377 | ``` 378 | 379 | ## Metrics 380 | 381 | Get current RAM, CPU, Disk & Network usage metrics. 382 | 383 | ```c# 384 | var metrics = await typesenseClient.RetrieveMetrics(); 385 | ``` 386 | 387 | ## Stats 388 | 389 | Get stats about API endpoints. 390 | 391 | ```c# 392 | var stats = await typesenseClient.RetrieveStats(); 393 | ``` 394 | 395 | ## Health 396 | 397 | Get stats about API endpoints. 398 | 399 | ```c# 400 | var health = await typesenseClient.RetrieveHealth(); 401 | ``` 402 | 403 | ## Snapshot 404 | 405 | Asynchronously initiates a snapshot operation on the Typesense server. 406 | 407 | ```c# 408 | var snapshotResponse = await typesenseClient.CreateSnapshot("/my_snapshot_path"); 409 | ``` 410 | ## Disk Compaction 411 | 412 | Asynchronously initiates the running of a compaction of the underlying RocksDB database. 413 | 414 | ```c# 415 | var diskCompactionResponse = await typesenseClient.CompactDisk(); 416 | ``` 417 | 418 | ### Typesense API Errors 419 | 420 | Typesense API exceptions in the [Typesense-api-errors](https://typesense.org/docs/0.23.0/api/api-errors.html) spec. 421 | 422 | | Type | Description | 423 | |:-------------------------------------------|:---------------------------------------------------------------------------| 424 | | `TypesenseApiException` | Base exception type for Typesense api exceptions. | 425 | | `TypesenseApiBadRequestException` | Bad Request - The request could not be understood due to malformed syntax. | 426 | | `TypesenseApiUnauthorizedException` | Unauthorized - Your API key is wrong. | 427 | | `TypesenseApiNotFoundException` | Not Found - The requested resource is not found. | 428 | | `TypesenseApiConflictException` | Conflict - When a resource already exists. | 429 | | `TypesenseApiUnprocessableEntityException` | Unprocessable Entity - Request is well-formed, but cannot be processed. | 430 | | `TypesenseApiServiceUnavailableException` | Service Unavailable - We’re temporarily offline. Please try again later. | 431 | 432 | ## Tests 433 | 434 | Running all tests. 435 | 436 | ```sh 437 | dotnet test 438 | ``` 439 | 440 | ### Running only unit tests 441 | ```sh 442 | dotnet test --filter Category=Unit 443 | ``` 444 | 445 | ### Running integration tests 446 | 447 | ```sh 448 | dotnet test --filter Category=Integration 449 | ``` 450 | 451 | To run integration tests, you can execute Typesense in a docker container by using the command below. The process utilizes the `/tmp/data` folder however, if you prefer to place it in a different directory, you can modify the path accordingly. 452 | 453 | 454 | ```sh 455 | docker run -p 8108:8108 -v/tmp/data:/data typesense/typesense:28.0 --data-dir /data --api-key=key 456 | ``` 457 | -------------------------------------------------------------------------------- /examples/Example/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using Typesense; 7 | using Typesense.Setup; 8 | 9 | namespace Example; 10 | 11 | sealed class Program 12 | { 13 | async static Task Main(string[] args) 14 | { 15 | // You can either create a new service collection or using an existing service collection. 16 | var provider = new ServiceCollection() 17 | .AddTypesenseClient(config => 18 | { 19 | config.ApiKey = "key"; 20 | config.Nodes = new List { new Node("localhost", "8108", "http") }; 21 | }).BuildServiceProvider(); 22 | 23 | var typesenseClient = provider.GetService(); 24 | 25 | // Example of how to create an collection. 26 | await ExampleCreateCollection(typesenseClient); 27 | 28 | // Example how to retrieve a collection. 29 | await ExampleRetrieveCollection(typesenseClient); 30 | 31 | // Example how to retrieve multiple collections. 32 | await ExampleRetrieveCollections(typesenseClient); 33 | 34 | // Example of doing single inserts. 35 | await ExampleInsertIntoCollection(typesenseClient); 36 | 37 | // Example export all documents from a collection. 38 | await ExampleExportAllDocumentsFromCollection(typesenseClient); 39 | 40 | // Example exporting with export parameters from a specified collection. 41 | await ExampleExportDocumentsWithExportParameters(typesenseClient); 42 | 43 | // Example upsert document, existing and not existing. 44 | await ExampleUpsertDocument(typesenseClient); 45 | 46 | // Example importing multiple documents. 47 | await ExampleImportDocuments(typesenseClient); 48 | 49 | // Example updating existing document. 50 | await ExampleUpdatingExistingDocument(typesenseClient); 51 | 52 | // Example doing a simple search. 53 | await ExampleDoingSearch(typesenseClient); 54 | 55 | // Example showcasing retrieveing document on id. 56 | await ExampleRetrieveDocumentOnId(typesenseClient); 57 | 58 | // Example handling not found document exception, to showcase TypesenseApiException handling. 59 | await ExampleHandlingTypesenseApiException(typesenseClient); 60 | 61 | // Example creating api keys. 62 | await ExampleCreatingApiKeys(typesenseClient); 63 | 64 | // Example retrieve api key. 65 | await ExampleRetrieveApiKey(typesenseClient); 66 | 67 | // Example list all api keys. 68 | await ExampleListApiKeys(typesenseClient); 69 | 70 | // Example creating scoped api key. 71 | ExampleGenerateScopedApiKey(typesenseClient); 72 | 73 | // Example upsert search override 74 | await ExampleUpsertSearchOverride(typesenseClient); 75 | 76 | // Example list all search overrides for a given collection. 77 | await ExampleListAllSearchOverridesForCollection(typesenseClient); 78 | 79 | // Example retrieve specific search override in a specific collection. 80 | await ExampleRetrieveSearchOverrideInCollection(typesenseClient); 81 | 82 | // Example upsert collectiona alias. 83 | await ExampleUpsertCollectionAlias(typesenseClient); 84 | 85 | // Example retrieve collection alias. 86 | await ExampleRetrieveCollectionAlias(typesenseClient); 87 | 88 | // Example list all collection aliases. 89 | await ExampleListAllCollectionAliases(typesenseClient); 90 | 91 | // Example upsert synonym 92 | await ExampleUpsertSynonym(typesenseClient); 93 | 94 | // Example retrieve synonym in collection. 95 | await ExampleRetrieveSynonymInCollection(typesenseClient); 96 | 97 | // Example list all synonyms in a collection. 98 | await ExampleListAllSynonymsInCollection(typesenseClient); 99 | 100 | // Example delete collection alias 101 | await ExampleDeleteCollectionAlias(typesenseClient); 102 | 103 | // Example delete synonym 104 | await ExampleDeleteSynonym(typesenseClient); 105 | 106 | // Example delete key(s) 107 | foreach (var key in (await typesenseClient.ListKeys()).Keys) 108 | await ExampleDeleteApiKey(typesenseClient, key.Id); 109 | 110 | // Example delete document 111 | await ExampleDeleteDocumentInCollection(typesenseClient); 112 | 113 | // Example delete documents in colleciton with filter 114 | await ExampleDeleteDocumentsWithFilter(typesenseClient); 115 | 116 | // Example delete search override 117 | await ExampleDeleteSearchOverride(typesenseClient); 118 | 119 | // Example update collection 120 | await ExampleUpdateCollection(typesenseClient); 121 | 122 | // Example delete collection 123 | await ExampleDeleteCollection(typesenseClient); 124 | 125 | // Example retrieve metrics 126 | await ExampleRetrieveMetrics(typesenseClient); 127 | 128 | // Example retrieve stats 129 | await ExampleRetrieveStats(typesenseClient); 130 | 131 | // Example retrieve health information 132 | await ExampleRetrieveHealth(typesenseClient); 133 | 134 | // Example create collection snapshot 135 | await ExampleCreateSnapshot(typesenseClient); 136 | 137 | // Example disk compaction 138 | await ExampleCompactDisk(typesenseClient); 139 | } 140 | 141 | private static async Task ExampleCreateCollection(ITypesenseClient typesenseClient) 142 | { 143 | var schema = new Schema( 144 | "Addresses", 145 | new List 146 | { 147 | new Field("id", FieldType.String, false), 148 | new Field("houseNumber", FieldType.Int32, false), 149 | new Field("accessAddress", FieldType.String, false, true), 150 | new Field("metadataNotes", FieldType.String, false, true, false), 151 | }, 152 | "houseNumber"); 153 | 154 | var createCollectionResponse = await typesenseClient.CreateCollection(schema); 155 | Console.WriteLine($"Created collection: {JsonSerializer.Serialize(createCollectionResponse)}"); 156 | } 157 | 158 | private static async Task ExampleRetrieveCollection(ITypesenseClient typesenseClient) 159 | { 160 | var retrieveCollection = await typesenseClient.RetrieveCollection("Addresses"); 161 | Console.WriteLine($"Retrieve collection: {JsonSerializer.Serialize(retrieveCollection)}"); 162 | } 163 | 164 | private static async Task ExampleRetrieveCollections(ITypesenseClient typesenseClient) 165 | { 166 | var retrieveCollections = await typesenseClient.RetrieveCollections(); 167 | Console.WriteLine($"Retrieve collections: {JsonSerializer.Serialize(retrieveCollections)}"); 168 | } 169 | 170 | private static async Task ExampleInsertIntoCollection(ITypesenseClient typesenseClient) 171 | { 172 | var addressOne = new Address 173 | { 174 | HouseNumber = 1, 175 | AccessAddress = "Smedgade 25B" 176 | }; 177 | 178 | // Example to show optional AccessAddress 179 | var addressTwo = new Address 180 | { 181 | HouseNumber = 2, 182 | }; 183 | 184 | // Example to show non indexed field 185 | var addressThree = new Address 186 | { 187 | HouseNumber = 3, 188 | AccessAddress = "Singel 12", 189 | MetadataNotes = "This field is not indexed and will not use memory." 190 | }; 191 | 192 | var houseOneResponse = await typesenseClient.CreateDocument
("Addresses", addressOne); 193 | Console.WriteLine($"Created document: {JsonSerializer.Serialize(houseOneResponse)}"); 194 | 195 | var houseTwoResponse = await typesenseClient.CreateDocument
("Addresses", addressTwo); 196 | Console.WriteLine($"Created document: {JsonSerializer.Serialize(addressTwo)}"); 197 | 198 | var houseThreeResponse = await typesenseClient.CreateDocument
("Addresses", addressThree); 199 | Console.WriteLine($"Created document: {JsonSerializer.Serialize(houseThreeResponse)}"); 200 | } 201 | 202 | private static async Task ExampleExportAllDocumentsFromCollection(ITypesenseClient typesenseClient) 203 | { 204 | var exportResult = await typesenseClient.ExportDocuments
("Addresses"); 205 | Console.WriteLine($"Export result: {JsonSerializer.Serialize(exportResult)}"); 206 | } 207 | 208 | private static async Task ExampleExportDocumentsWithExportParameters(ITypesenseClient typesenseClient) 209 | { 210 | var exportResultParameters = await typesenseClient.ExportDocuments
( 211 | "Addresses", 212 | new ExportParameters 213 | { 214 | IncludeFields = "houseNumber" 215 | }); 216 | 217 | Console.WriteLine($"Export result: {JsonSerializer.Serialize(exportResultParameters)}"); 218 | } 219 | 220 | private static async Task ExampleUpsertDocument(ITypesenseClient typesenseClient) 221 | { 222 | // Example showcasing upserting a new address that is not already indexed. 223 | var notExistingAddress = new Address 224 | { 225 | HouseNumber = 25, 226 | AccessAddress = "Address" 227 | }; 228 | 229 | var upsertNotExistingAddress = await typesenseClient.UpsertDocument
( 230 | "Addresses", notExistingAddress); 231 | 232 | Console.WriteLine($"Upserted document: {JsonSerializer.Serialize(upsertNotExistingAddress)}"); 233 | 234 | // Example showcasing upserting a new address that is already indexed. 235 | var existingAddress = new Address 236 | { 237 | Id = upsertNotExistingAddress.Id, 238 | HouseNumber = 25, 239 | AccessAddress = "Awesome new access address!" 240 | }; 241 | 242 | var upsertExistingAddress = await typesenseClient.UpsertDocument
( 243 | "Addresses", existingAddress); 244 | 245 | Console.WriteLine($"Upserted document: {JsonSerializer.Serialize(upsertExistingAddress)}"); 246 | } 247 | 248 | private static async Task ExampleImportDocuments(ITypesenseClient typesenseClient) 249 | { 250 | var addresses = new List
251 | { 252 | new Address { AccessAddress = "Sulstreet 4", HouseNumber = 223 }, 253 | new Address { AccessAddress = "Sulstreet 24", HouseNumber = 321 } 254 | }; 255 | 256 | var importDocuments = await typesenseClient.ImportDocuments
( 257 | "Addresses", addresses, 40, ImportType.Create); 258 | 259 | Console.WriteLine($"Import documents: {JsonSerializer.Serialize(importDocuments)}"); 260 | } 261 | 262 | private static async Task ExampleUpdatingExistingDocument(ITypesenseClient typesenseClient) 263 | { 264 | var address = await typesenseClient.RetrieveDocument
("Addresses", "4"); 265 | 266 | address.HouseNumber = 1; 267 | 268 | var updateDocumentResult = await typesenseClient.UpdateDocument
("Addresses", "4", address); 269 | Console.WriteLine($"Updated document: ${JsonSerializer.Serialize(updateDocumentResult)}"); 270 | } 271 | 272 | private static async Task ExampleDoingSearch(ITypesenseClient typesenseClient) 273 | { 274 | var query = new SearchParameters("Sul", "accessAddress"); 275 | var searchResult = await typesenseClient.Search
("Addresses", query); 276 | Console.WriteLine($"Search result: {JsonSerializer.Serialize(searchResult)}"); 277 | } 278 | 279 | private static async Task ExampleRetrieveDocumentOnId(ITypesenseClient typesenseClient) 280 | { 281 | var retrievedDocument = await typesenseClient.RetrieveDocument
("Addresses", "1"); 282 | Console.WriteLine($"Retrieved document: {JsonSerializer.Serialize(retrievedDocument)}"); 283 | } 284 | 285 | private static async Task ExampleHandlingTypesenseApiException(ITypesenseClient typesenseClient) 286 | { 287 | try 288 | { 289 | var documentNotExist = await typesenseClient.RetrieveDocument
("Addresses", "1120"); 290 | } 291 | catch (TypesenseApiNotFoundException e) 292 | { 293 | Console.WriteLine(e.Message); 294 | } 295 | } 296 | 297 | private static async Task ExampleCreatingApiKeys(ITypesenseClient typesenseClient) 298 | { 299 | var keyOne = new Key( 300 | "Example key one", 301 | new[] { "*" }, 302 | new[] { "*" }) 303 | { 304 | Value = "Example-api-1-key-value", 305 | ExpiresAt = 1661344547 306 | }; 307 | 308 | var keyTwo = new Key( 309 | "Example key two", 310 | new[] { "*" }, 311 | new[] { "*" }) 312 | { 313 | Value = "Example-api-2-key-value", 314 | ExpiresAt = 1661344547 315 | }; 316 | 317 | var createKeyResultOne = await typesenseClient.CreateKey(keyOne); 318 | Console.WriteLine($"Created key: {JsonSerializer.Serialize(createKeyResultOne)}"); 319 | 320 | var createKeyResultTwo = await typesenseClient.CreateKey(keyTwo); 321 | Console.WriteLine($"Created key: {JsonSerializer.Serialize(createKeyResultTwo)}"); 322 | } 323 | 324 | private static async Task ExampleRetrieveApiKey(ITypesenseClient typesenseClient) 325 | { 326 | var retrievedKey = await typesenseClient.RetrieveKey(1); 327 | Console.WriteLine($"Retrieved key: {JsonSerializer.Serialize(retrievedKey)}"); 328 | } 329 | 330 | private static async Task ExampleListApiKeys(ITypesenseClient typesenseClient) 331 | { 332 | var listKeys = await typesenseClient.ListKeys(); 333 | Console.WriteLine($"List keys: {JsonSerializer.Serialize(listKeys)}"); 334 | } 335 | 336 | private static void ExampleGenerateScopedApiKey(ITypesenseClient typesenseClient) 337 | { 338 | var scopedSearchKey = typesenseClient.GenerateScopedSearchKey( 339 | "my-awesome-security-key", "{\"filter_by\":\"accessible_to_user_ids:2\"}"); 340 | 341 | Console.WriteLine($"Scoped Search Key: {scopedSearchKey}"); 342 | } 343 | 344 | private static async Task ExampleUpsertSearchOverride(ITypesenseClient typesenseClient) 345 | { 346 | var searchOverride = new SearchOverride(new Rule("Sul", "exact")) 347 | { 348 | Includes = new List { new Include("2", 1) }, 349 | }; 350 | 351 | var upsertSearchOverrideResponse = await typesenseClient.UpsertSearchOverride( 352 | "Addresses", 353 | "addresses-override", 354 | searchOverride); 355 | 356 | Console.WriteLine($"Upsert search override: {JsonSerializer.Serialize(upsertSearchOverrideResponse)}"); 357 | } 358 | 359 | private static async Task ExampleListAllSearchOverridesForCollection(ITypesenseClient typesenseClient) 360 | { 361 | var listSearchOverrides = await typesenseClient.ListSearchOverrides("Addresses"); 362 | Console.WriteLine($"List search overrides: {JsonSerializer.Serialize(listSearchOverrides)}"); 363 | } 364 | 365 | 366 | private static async Task ExampleRetrieveSearchOverrideInCollection(ITypesenseClient typesenseClient) 367 | { 368 | var retrieveSearchOverride = await typesenseClient.RetrieveSearchOverride( 369 | "Addresses", "addresses-override"); 370 | Console.WriteLine($"retrieve search override: {JsonSerializer.Serialize(retrieveSearchOverride)}"); 371 | } 372 | 373 | private static async Task ExampleUpsertCollectionAlias(ITypesenseClient typesenseClient) 374 | { 375 | var upsertCollectionAlias = await typesenseClient.UpsertCollectionAlias( 376 | "Address_Alias", new CollectionAlias("Addresses")); 377 | 378 | Console.WriteLine($"Upsert alias: {JsonSerializer.Serialize(upsertCollectionAlias)}"); 379 | } 380 | 381 | private static async Task ExampleRetrieveCollectionAlias(ITypesenseClient typesenseClient) 382 | { 383 | var retrieveCollectionAlias = await typesenseClient.RetrieveCollectionAlias("Address_Alias"); 384 | Console.WriteLine($"retrieve alias: {JsonSerializer.Serialize(retrieveCollectionAlias)}"); 385 | } 386 | 387 | private static async Task ExampleListAllCollectionAliases(ITypesenseClient typesenseClient) 388 | { 389 | var listCollectionAliases = await typesenseClient.ListCollectionAliases(); 390 | Console.WriteLine($"List alias: {JsonSerializer.Serialize(listCollectionAliases)}"); 391 | } 392 | 393 | private static async Task ExampleUpsertSynonym(ITypesenseClient typesenseClient) 394 | { 395 | var upsertSynonym = await typesenseClient.UpsertSynonym( 396 | "Addresses", "Address_Synonym", new SynonymSchema(new List { "Sultan", "Soltan", "Softan" })); 397 | 398 | Console.WriteLine($"Upsert synonym: {JsonSerializer.Serialize(upsertSynonym)}"); 399 | } 400 | 401 | private static async Task ExampleRetrieveSynonymInCollection(ITypesenseClient typesenseClient) 402 | { 403 | var retrieveSynonym = await typesenseClient.RetrieveSynonym("Addresses", "Address_Synonym"); 404 | Console.WriteLine($"Retrieve synonym: {JsonSerializer.Serialize(retrieveSynonym)}"); 405 | } 406 | 407 | private static async Task ExampleListAllSynonymsInCollection(ITypesenseClient typesenseClient) 408 | { 409 | var listSynonyms = await typesenseClient.ListSynonyms("Addresses"); 410 | Console.WriteLine($"List synonyms: {JsonSerializer.Serialize(listSynonyms)}"); 411 | } 412 | 413 | private static async Task ExampleDeleteCollectionAlias(ITypesenseClient typesenseClient) 414 | { 415 | var deleteCollectionAlias = await typesenseClient.DeleteCollectionAlias("Address_Alias"); 416 | Console.WriteLine($"Delete alias: {JsonSerializer.Serialize(deleteCollectionAlias)}"); 417 | } 418 | 419 | private static async Task ExampleDeleteSynonym(ITypesenseClient typesenseClient) 420 | { 421 | var deleteSynonym = await typesenseClient.DeleteSynonym("Addresses", "Address_Synonym"); 422 | Console.WriteLine($"Delete synonym: {JsonSerializer.Serialize(deleteSynonym)}"); 423 | } 424 | 425 | private static async Task ExampleDeleteApiKey(ITypesenseClient typesenseClient, int id) 426 | { 427 | var deletedKey = await typesenseClient.DeleteKey(id); 428 | Console.WriteLine($"Deleted key: {JsonSerializer.Serialize(deletedKey)}"); 429 | } 430 | 431 | private static async Task ExampleDeleteDocumentInCollection(ITypesenseClient typesenseClient) 432 | { 433 | var deleteResult = await typesenseClient.DeleteDocument
("Addresses", "2"); 434 | Console.WriteLine($"Deleted document {JsonSerializer.Serialize(deleteResult)}"); 435 | } 436 | 437 | private static async Task ExampleDeleteDocumentsWithFilter(ITypesenseClient typesenseClient) 438 | { 439 | var deleteFilterResult = await typesenseClient.DeleteDocuments("Addresses", "houseNumber:>=3", 100); 440 | Console.WriteLine($"Deleted amount: {deleteFilterResult.NumberOfDeleted}"); 441 | } 442 | 443 | private static async Task ExampleDeleteSearchOverride(ITypesenseClient typesenseClient) 444 | { 445 | var deletedSearchOverrideResult = await typesenseClient.DeleteSearchOverride( 446 | "Addresses", "addresses-override"); 447 | 448 | Console.WriteLine($"Deleted override: {JsonSerializer.Serialize(deletedSearchOverrideResult)}"); 449 | } 450 | 451 | private static async Task ExampleDeleteCollection(ITypesenseClient typesenseClient) 452 | { 453 | var deleteCollectionResult = await typesenseClient.DeleteCollection("Addresses"); 454 | Console.WriteLine($"Deleted collection: {JsonSerializer.Serialize(deleteCollectionResult)}"); 455 | } 456 | 457 | private static async Task ExampleUpdateCollection(ITypesenseClient typesenseClient) 458 | { 459 | var updateSchema = new UpdateSchema(new List 460 | { 461 | // Example deleting existing field. 462 | new UpdateSchemaField("metadataNotes", drop: true), 463 | // Example adding a new field. 464 | new UpdateSchemaField("city", FieldType.String, facet: false, optional: true) 465 | }); 466 | 467 | var updateCollectionResponse = await typesenseClient.UpdateCollection( 468 | "Addresses", 469 | updateSchema); 470 | 471 | Console.WriteLine($"Updated collection: {JsonSerializer.Serialize(updateCollectionResponse)}"); 472 | } 473 | 474 | private static async Task ExampleRetrieveMetrics(ITypesenseClient typesenseClient) 475 | { 476 | var metrics = await typesenseClient.RetrieveMetrics(); 477 | Console.WriteLine($"Retrieved metrics: {JsonSerializer.Serialize(metrics)}"); 478 | } 479 | 480 | private static async Task ExampleRetrieveStats(ITypesenseClient typesenseClient) 481 | { 482 | var stats = await typesenseClient.RetrieveStats(); 483 | Console.WriteLine($"Retrieved stats: {JsonSerializer.Serialize(stats)}"); 484 | } 485 | 486 | private static async Task ExampleRetrieveHealth(ITypesenseClient typesenseClient) 487 | { 488 | var health = await typesenseClient.RetrieveHealth(); 489 | Console.WriteLine($"Retrieved stats: {JsonSerializer.Serialize(health)}"); 490 | } 491 | 492 | private static async Task ExampleCreateSnapshot(ITypesenseClient typesenseClient) 493 | { 494 | var snapshotResponse = await typesenseClient.CreateSnapshot("/my_snapshot_path"); 495 | Console.WriteLine($"Snapshot: {JsonSerializer.Serialize(snapshotResponse)}"); 496 | } 497 | 498 | private static async Task ExampleCompactDisk(ITypesenseClient typesenseClient) 499 | { 500 | var compactDiskResponse = await typesenseClient.CompactDisk(); 501 | Console.WriteLine($"Compact disk: {JsonSerializer.Serialize(compactDiskResponse)}"); 502 | } 503 | } 504 | -------------------------------------------------------------------------------- /src/Typesense/SearchParameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using Typesense.Converter; 4 | 5 | namespace Typesense; 6 | 7 | public enum SplitJoinTokenOption 8 | { 9 | Fallback, 10 | Always, 11 | Off 12 | } 13 | 14 | public record MultiSearchParameters : SearchParameters 15 | { 16 | /// 17 | /// The collection to query. 18 | /// 19 | [JsonPropertyName("collection")] 20 | public string Collection { get; set; } 21 | 22 | // --------------------------------------------------------------------------------------- 23 | // Vector Search - https://typesense.org/docs/0.24.1/api/vector-search.html#what-is-an-embedding 24 | // --------------------------------------------------------------------------------------- 25 | 26 | /// 27 | /// Query-string for vector searches. 28 | /// 29 | [JsonConverter(typeof(VectorQueryJsonConverter)), JsonPropertyName("vector_query")] 30 | public VectorQuery? VectorQuery { get; init; } 31 | 32 | [JsonPropertyName("group_by")] 33 | public string GroupBy { get; set; } 34 | 35 | [JsonPropertyName("group_limit")] 36 | public int? GroupLimit { get; set; } 37 | 38 | [JsonPropertyName("group_missing_values")] 39 | public bool? GroupMissingValues { get; set; } 40 | 41 | /// 42 | /// Set this parameter to the value of a preset that has been created in typesense. 43 | /// The query parameters of the preset will then be used in your search. 44 | /// 45 | [JsonPropertyName("preset")] 46 | public string? Preset { get; set; } 47 | 48 | public MultiSearchParameters(string collection, string text) : base(text) 49 | { 50 | Collection = collection; 51 | } 52 | 53 | public MultiSearchParameters(string collection, string text, string queryBy) : base(text, queryBy) 54 | { 55 | Collection = collection; 56 | } 57 | } 58 | 59 | public record SearchParameters 60 | { 61 | // ------------------------------------------------------------------------------------- 62 | // Query parameters - https://typesense.org/docs/latest/api/search.html#query-parameters 63 | // ------------------------------------------------------------------------------------- 64 | 65 | /// 66 | /// The query text to search for in the collection. 67 | /// Use * as the search string to return all documents. 68 | /// This is typically useful when used in conjunction with filter_by. 69 | /// 70 | [JsonPropertyName("q")] 71 | public string Text { get; set; } 72 | 73 | /// 74 | /// A list of `string` fields that should be queried against. Multiple fields are separated with a comma. 75 | /// 76 | [JsonPropertyName("query_by")] 77 | public string? QueryBy { get; set; } 78 | 79 | /// 80 | /// Boolean field to indicate that the last word in the query should 81 | /// be treated as a prefix, and not as a whole word. This is used for building 82 | /// autocomplete and instant search interfaces. Defaults to true. 83 | /// 84 | [JsonPropertyName("prefix")] 85 | public bool? Prefix { get; set; } 86 | 87 | /// 88 | /// If infix index is enabled for this field, infix searching can be done on a per-field basis by sending a comma separated string parameter called infix to the search query. 89 | /// This parameter can have 3 values: 90 | /// off: infix search is disabled, which is default 91 | /// always: infix search is performed along with regular search 92 | /// fallback: infix search is performed if regular search does not produce results 93 | /// 94 | [JsonPropertyName("infix")] 95 | public string? Infix { get; set; } 96 | 97 | /// 98 | /// Set this parameter to the value of a preset that has been created in typesense. 99 | /// The query parameters of the preset will then be used in your search. 100 | /// 101 | [JsonPropertyName("preset")] 102 | public string? Preset { get; set; } 103 | 104 | /// 105 | /// Set this parameter to true if you wish to split the search query into space separated words yourself. 106 | /// When set to true, we will only split the search query by space, 107 | /// instead of using the locale-aware, built-in tokenizer. 108 | /// 109 | [JsonPropertyName("pre_segmented_query")] 110 | public bool? PreSegmentedQuery { get; set; } 111 | 112 | /// 113 | /// A comma separated list of words to be dropped from the search query while searching. 114 | /// 115 | [JsonPropertyName("stopwords")] 116 | public string? Stopwords { get; set; } 117 | 118 | /// 119 | /// Controls whether Typesense should validate if the fields exist in the schema. 120 | /// When set to false, Typesense will not throw an error if a field is missing. 121 | /// This is useful for programmatic grouping where not all fields may exist. 122 | /// 123 | [JsonPropertyName("validate_field_names")] 124 | public bool? ValidateFieldNames { get; set; } 125 | 126 | // --------------------------------------------------------------------------------------- 127 | // Filter parameters - https://typesense.org/docs/latest/api/search.html#filter-parameters 128 | // --------------------------------------------------------------------------------------- 129 | 130 | /// 131 | /// Filter conditions for refining your search results. 132 | /// Separate multiple conditions with &&. 133 | /// 134 | [JsonPropertyName("filter_by")] 135 | public string? FilterBy { get; set; } 136 | 137 | /// 138 | /// Applies the filtering operation incrementally / lazily. 139 | /// Set this to true when you are potentially filtering on large values but the tokens in the query are expected to match very few documents. 140 | /// Default: false 141 | /// 142 | [JsonPropertyName("enable_lazy_filter")] 143 | public bool? EnableLazyFilter { get; set; } 144 | 145 | /// 146 | /// Controls the number of similar words that Typesense considers during fuzzy search on filter_by values. 147 | /// Useful for controlling prefix matches like company_name:Acm*. 148 | /// Default: 4 149 | /// 150 | [JsonPropertyName("max_filter_by_candidates")] 151 | public int? MaxFilterByCandidates { get; set; } 152 | 153 | // --------------------------------------------------------------------------------------- 154 | // Ranking and Sorting parameters - https://typesense.org/docs/latest/api/search.html#ranking-and-sorting-parameters 155 | // --------------------------------------------------------------------------------------- 156 | 157 | /// 158 | /// The relative weight to give each `query_by` field when ranking results. 159 | /// This can be used to boost fields in priority, when looking for matches. 160 | /// Multiple fields are separated with a comma. 161 | /// 162 | [JsonPropertyName("query_by_weights")] 163 | public string? QueryByWeights { get; set; } 164 | 165 | /// 166 | /// In a multi-field matching context, this parameter determines how the representative text match score of a record is calculated. 167 | /// This parameter can have 2 values: 168 | /// max_score: the best text match score across all fields are used as the representative score of this record. Field weights are used as tie breakers when 2 records share the same text match score. 169 | /// max_weight: the text match score of the highest weighted field is used as the representative text relevancy score of the record. 170 | /// 171 | [JsonPropertyName("text_match_type")] 172 | public string? TextMatchType { get; set; } 173 | 174 | /// 175 | /// A list of numerical fields and their corresponding sort orders 176 | /// that will be used for ordering your results. 177 | /// Up to 3 sort fields can be specified. 178 | /// The text similarity score is exposed as a special `_text_match` field that 179 | /// you can use in the list of sorting fields. 180 | /// If no `sort_by` parameter is specified, results are sorted by 181 | /// `_text_match:desc,default_sorting_field:desc` 182 | /// 183 | [JsonPropertyName("sort_by")] 184 | public string? SortBy { get; set; } 185 | 186 | /// 187 | /// By default, Typesense prioritizes documents whose field value matches 188 | /// exactly with the query. Set this parameter to `false` to disable this behavior. 189 | /// Defaults to true. 190 | /// 191 | [JsonPropertyName("prioritize_exact_match")] 192 | public bool? PrioritizeExactMatch { get; set; } 193 | 194 | /// 195 | /// Make Typesense prioritize documents where the query words appear earlier in the text. 196 | /// 197 | [JsonPropertyName("prioritize_token_position")] 198 | public bool? PrioritizeTokenPosition { get; set; } 199 | 200 | /// 201 | /// Make Typesense prioritize documents where the query words appear in more number of fields. 202 | /// Default: true 203 | /// 204 | [JsonPropertyName("prioritize_num_matching_fields")] 205 | public bool? PrioritizeNumberMatchingFields { get; set; } 206 | 207 | /// 208 | /// A list of records to unconditionally include in the search results 209 | /// at specific positions. An example use case would be to feature or promote 210 | /// certain items on the top of search results. 211 | /// A list of `record_id:hit_position`. Eg: to include a record with ID 123 212 | /// at Position 1 and another record with ID 456 at Position 5, 213 | /// you'd specify `123:1,456:5`. 214 | /// You could also use the Overrides feature to override search results based 215 | /// on rules. Overrides are applied first, followed by `pinned_hits` and 216 | /// finally `hidden_hits`. 217 | /// 218 | [JsonPropertyName("pinned_hits")] 219 | public string? PinnedHits { get; set; } 220 | 221 | /// 222 | /// A list of records to unconditionally hide from search results. 223 | /// A list of `record_id`s to hide. Eg: to hide records with IDs 123 and 456, 224 | /// you'd specify `123,456`. 225 | /// You could also use the Overrides feature to override search results based 226 | /// on rules. Overrides are applied first, followed by `pinned_hits` and 227 | /// finally `hidden_hits`. 228 | /// 229 | [JsonPropertyName("hidden_hits")] 230 | public string? HiddenHits { get; set; } 231 | 232 | /// 233 | /// Whether the filter_by condition of the search query should be applicable to curated results (override definitions, pinned hits, hidden hits, etc.). 234 | /// 235 | [JsonPropertyName("filter_curated_hits")] 236 | public bool? FilterCuratedHits { get; set; } 237 | 238 | /// 239 | /// If you have some overrides defined but want to disable all of them during 240 | /// query time, you can do that by setting this parameter to false 241 | /// 242 | [JsonPropertyName("enable_overrides")] 243 | public bool? EnableOverrides { get; set; } 244 | 245 | /// 246 | /// You can trigger particular override rules that you've tagged using their tag name(s) in this search parameter. 247 | /// 248 | [JsonPropertyName("override_tags")] 249 | public string? OverrideTags { get; set; } 250 | 251 | /// 252 | /// If you have some synonyms defined but want to disable all of them for a particular search query, set enable_synonyms to false. 253 | /// Default: true 254 | /// 255 | [JsonPropertyName("enable_synonyms")] 256 | public bool? EnableSynonyms { get; set; } 257 | 258 | /// 259 | /// Allow synonym resolution on word prefixes in the query. 260 | /// Default: false 261 | /// 262 | [JsonPropertyName("synonym_prefix")] 263 | public bool? SynonymPrefix { get; set; } 264 | 265 | // --------------------------------------------------------------------------------------- 266 | // Pagination parameters - https://typesense.org/docs/latest/api/search.html#pagination-parameters 267 | // --------------------------------------------------------------------------------------- 268 | 269 | /// 270 | /// Results from this specific page number would be fetched. 271 | /// 272 | [JsonPropertyName("page")] 273 | public int? Page { get; set; } 274 | 275 | /// 276 | /// Number of results to fetch per page. Default: 10 277 | /// 278 | [JsonPropertyName("per_page")] 279 | public int? PerPage { get; set; } 280 | 281 | /// 282 | /// Identifies the starting point to return hits from a result set. Can be used as an alternative to the page parameter. 283 | /// 284 | [JsonPropertyName("offset")] 285 | public int? Offset { get; set; } 286 | 287 | /// 288 | /// Number of hits to fetch. Can be used as an alternative to the per_page parameter. Default: 10. 289 | /// 290 | [JsonPropertyName("limit")] 291 | public int? Limit { get; set; } 292 | 293 | /// 294 | /// Maximum number of hits returned. Increasing this value might 295 | /// increase search latency. Default: 500. Use `all` to return all hits found. 296 | /// 297 | [Obsolete("max_hits has been deprecated since Typesense version 0.19.0")] 298 | [JsonPropertyName("max_hits")] 299 | public int? MaxHits { get; set; } 300 | 301 | // --------------------------------------------------------------------------------------- 302 | // Faceting parameters - https://typesense.org/docs/latest/api/search.html#faceting-parameters 303 | // --------------------------------------------------------------------------------------- 304 | 305 | /// 306 | /// A list of fields that will be used for faceting your results 307 | /// on. Separate multiple fields with a comma. 308 | /// 309 | [JsonPropertyName("facet_by")] 310 | public string? FacetBy { get; set; } 311 | 312 | /// 313 | /// Typesense supports two strategies for efficient faceting, and has some built-in heuristics to pick the right strategy for you. 314 | /// The valid values for this parameter are exhaustive, top_values and automatic (default). 315 | /// Read more: https://typesense.org/docs/latest/api/search.html#faceting-parameters 316 | /// 317 | [JsonPropertyName("facet_strategy")] 318 | public string? FacetStrategy { get; set; } 319 | 320 | /// 321 | /// Maximum number of facet values to be returned. 322 | /// 323 | [JsonPropertyName("max_facet_values")] 324 | public int? MaxFacetValues { get; set; } 325 | 326 | /// 327 | /// Facet values that are returned can now be filtered via this parameter. 328 | /// The matching facet text is also highlighted. For example, when faceting 329 | /// by `category`, you can set `facet_query=category:shoe` to return only 330 | /// facet values that contain the prefix "shoe". 331 | /// 332 | [JsonPropertyName("facet_query")] 333 | public string? FacetQuery { get; set; } 334 | 335 | /// 336 | /// Controls the fuzziness of the facet query filter. 337 | /// 338 | [JsonPropertyName("facet_query_num_typos")] 339 | public int? FacetQueryNumberTypos { get; set; } 340 | 341 | [JsonPropertyName("facet_return_parent")] 342 | public string? FacetReturnParent { get; set; } 343 | 344 | /// 345 | /// Facet sampling is helpful to improve facet computation speed for large datasets, where the exact count is not required in the UI. 346 | /// Default: 100 (sampling is disabled by default) 347 | /// 348 | [JsonPropertyName("facet_sample_percent")] 349 | public int? FacetSamplePercent { get; set; } 350 | 351 | /// 352 | /// Facet sampling is helpful to improve facet computation speed for large datasets, where the exact count is not required in the UI. 353 | /// Default: 0 354 | /// 355 | [JsonPropertyName("facet_sample_threshold")] 356 | public int? FacetSampleThreshold { get; set; } 357 | 358 | // --------------------------------------------------------------------------------------- 359 | // Results parameters - https://typesense.org/docs/latest/api/search.html#results-parameters 360 | // --------------------------------------------------------------------------------------- 361 | 362 | /// 363 | /// List of fields from the document to include in the search result. 364 | /// 365 | [JsonPropertyName("include_fields")] 366 | public string? IncludeFields { get; set; } 367 | 368 | /// 369 | /// List of fields from the document to exclude in the search result. 370 | /// 371 | [JsonPropertyName("exclude_fields")] 372 | public string? ExcludeFields { get; set; } 373 | 374 | /// 375 | /// List of fields which should be highlighted fully without snippeting. 376 | /// 377 | [JsonPropertyName("highlight_full_fields")] 378 | public string? HighlightFullFields { get; set; } 379 | 380 | /// 381 | /// Comma separated list of fields that should be highlighted with snippetting. 382 | /// Default: all queried fields will be highlighted. 383 | /// Set to none to disable snippetting fully. 384 | /// 385 | [JsonPropertyName("highlight_fields")] 386 | public string? HighlightFields { get; set; } 387 | 388 | /// 389 | /// The number of tokens that should surround the highlighted text on each side. 390 | /// 391 | [JsonPropertyName("highlight_affix_num_tokens")] 392 | public int? HighlightAffixNumberOfTokens { get; set; } 393 | 394 | /// 395 | /// The start tag used for the highlighted snippets. 396 | /// 397 | [JsonPropertyName("highlight_start_tag")] 398 | public string? HighlightStartTag { get; set; } 399 | 400 | /// 401 | /// The end tag used for the highlighted snippets. 402 | /// 403 | [JsonPropertyName("highlight_end_tag")] 404 | public string? HighlightEndTag { get; set; } 405 | 406 | /// 407 | /// Field values under this length will be fully highlighted, instead of showing 408 | /// a snippet of relevant portion. Default: 30 409 | /// 410 | [JsonPropertyName("snippet_threshold")] 411 | public int? SnippetThreshold { get; set; } 412 | 413 | /// 414 | /// Maximum number of hits that can be fetched from the collection. Eg: 200 415 | /// page * per_page should be less than this number for the search request to return results. 416 | /// A list of custom fields that must be highlighted even if you don't query 417 | /// for them. 418 | /// 419 | [JsonPropertyName("limit_hits")] 420 | public int? LimitHits { get; set; } 421 | 422 | /// 423 | /// Typesense will attempt to return results early if the cutoff time has elapsed. 424 | /// This is not a strict guarantee and facet computation is not bound by this parameter. 425 | /// Default: no search cutoff happens. 426 | /// 427 | [JsonPropertyName("search_cutoff_ms")] 428 | public int? SearchCutoffMs { get; set; } 429 | 430 | /// 431 | /// Control the number of words that Typesense considers for typo and prefix searching. 432 | /// Default: 4 (or 10000 if exhaustive_search is enabled). 433 | /// 434 | [JsonPropertyName("max_candidates")] 435 | public int? MaxCandidates { get; set; } 436 | 437 | /// 438 | /// Whether all variations of prefixes and typo corrections should be considered, 439 | /// without stopping early when enough results are found. 440 | /// Ignores DropTokensThreshold and TypoTokensThreshold. 441 | /// 442 | [JsonPropertyName("exhaustive_search")] 443 | public bool? ExhaustiveSearch { get; set; } 444 | 445 | // --------------------------------------------------------------------------------------- 446 | // Typo-Tolerance parameters - https://typesense.org/docs/latest/api/search.html#typo-tolerance-parameters 447 | // --------------------------------------------------------------------------------------- 448 | 449 | /// 450 | /// The number of typographical errors (1 or 2) that would be tolerated. 451 | /// 452 | [JsonPropertyName("num_typos")] 453 | public string? NumberOfTypos { get; set; } 454 | 455 | /// 456 | /// Minimum word length for 1-typo correction to be applied. The value 457 | /// of `num_typos` is still treated as the maximum allowed typos. 458 | /// Default: 4. 459 | /// 460 | [JsonPropertyName("min_len_1typo")] 461 | public int? MinLen1Typo { get; set; } 462 | 463 | /// 464 | /// Minimum word length for 2-typo correction to be applied. The value 465 | /// of `num_typos` is still treated as the maximum allowed typos. 466 | /// Default: 7. 467 | /// 468 | [JsonPropertyName("min_len_2typo")] 469 | public int? MinLen2Typo { get; set; } 470 | 471 | /// 472 | /// Treat space as typo: search for q=basket ball if q=basketball is not found or vice-versa. 473 | /// 474 | [JsonPropertyName("split_join_tokens")] 475 | public SplitJoinTokenOption? SplitJoinTokens { get; set; } 476 | 477 | /// 478 | /// If the number of results found for a specific query is less than this number, 479 | /// Typesense will attempt to look for tokens with more typos until 480 | /// enough results are found. Default: 100 481 | /// 482 | [JsonPropertyName("typo_tokens_threshold")] 483 | public int? TypoTokensThreshold { get; set; } 484 | 485 | /// 486 | /// If the number of results found for a specific query is less than 487 | /// this number, Typesense will attempt to drop the tokens in the query until 488 | /// enough results are found. Tokens that have the least individual hits 489 | /// are dropped first. Set to 0 to disable. Default: 10 490 | /// 491 | [JsonPropertyName("drop_tokens_threshold")] 492 | public int? DropTokensThreshold { get; set; } 493 | 494 | /// 495 | /// Dictates the direction in which the words in the query must be dropped when the original words in the query do not appear in any document. 496 | /// 497 | /// Values: right_to_left (default), left_to_right, both_sides:3 498 | /// A note on both_sides:3 - for queries upto 3 tokens (words) in length, this mode will drop tokens from both sides and exhaustively rank all matching results. 499 | /// If query length is greater than 3 words, Typesense will just fallback to default behavior of right_to_left 500 | /// 501 | [JsonPropertyName("drop_tokens_mode")] 502 | public string? DropTokensMode { get; set; } 503 | 504 | /// 505 | /// Set this parameter to false to disable typos on numerical query tokens. Default: true 506 | /// 507 | [JsonPropertyName("enable_typos_for_numerical_tokens")] 508 | public bool? EnableTyposForNumericalTokens { get; set; } 509 | 510 | /// 511 | /// Set this parameter to false to disable typos on alphanumerical query tokens. Default: true 512 | /// 513 | [JsonPropertyName("enable_typos_for_alpha_numerical_tokens")] 514 | public bool? EnableTyposForAlphaNumericalTokens { get; set; } 515 | 516 | /// 517 | /// Allow synonym resolution on typo-corrected words in the query. 518 | /// Default: 0 519 | /// 520 | [JsonPropertyName("synonym_num_typos")] 521 | public int? SynonymNumberTypos { get; set; } 522 | 523 | // --------------------------------------------------------------------------------------- 524 | // Caching parameters - https://typesense.org/docs/latest/api/search.html#caching-parameters 525 | // --------------------------------------------------------------------------------------- 526 | 527 | /// 528 | /// Enable server side caching of search query results. By default, caching is disabled. 529 | /// Default: false 530 | /// 531 | [JsonPropertyName("use_cache")] 532 | public bool? UseCache { get; set; } 533 | 534 | /// 535 | /// The duration (in seconds) that determines how long the search query is cached. 536 | /// This value can only be set as part of a scoped API key. 537 | /// Default: 60 538 | /// 539 | [JsonPropertyName("cache_ttl")] 540 | public int? CacheTtl { get; set; } 541 | 542 | /// 543 | /// How long to wait until an API call to a remote embedding service is considered a timeout. 544 | /// 545 | [JsonPropertyName("remote_embedding_timeout_ms")] 546 | public int? RemoteEmbeddingTimeoutMs { get; set; } 547 | 548 | /// 549 | /// When set to true, enables both text match and vector distance scores to be computed for all hybrid search results, 550 | /// improving the ranking by combining both score types for documents found only by keyword or vector search 551 | /// Default: false 552 | /// 553 | [JsonPropertyName("rerank_hybrid_matches")] 554 | public bool? RerankHybridMatches { get; set; } 555 | 556 | /// 557 | /// The number of times to retry an API call to a remote embedding service on failure. 558 | /// 559 | [JsonPropertyName("remote_embedding_num_tries")] 560 | public int? RemoteEmbeddingNumTries { get; set; } 561 | 562 | /// < 563 | /// When set to false, it will ignore any analityc roles 564 | /// 565 | [JsonPropertyName("enable_analytics")] 566 | public bool? EnableAnalytics { get; set; } 567 | 568 | [Obsolete("Use multi-arity constructor instead.")] 569 | public SearchParameters() 570 | { 571 | Text = string.Empty; 572 | QueryBy = string.Empty; 573 | } 574 | 575 | public SearchParameters(string text) 576 | { 577 | Text = text; 578 | } 579 | 580 | public SearchParameters(string text, string queryBy) 581 | { 582 | Text = text; 583 | QueryBy = queryBy; 584 | } 585 | } 586 | 587 | public record GroupedSearchParameters : SearchParameters 588 | { 589 | // --------------------------------------------------------------------------------------- 590 | // Grouping parameters - https://typesense.org/docs/latest/api/search.html#grouping-parameters 591 | // --------------------------------------------------------------------------------------- 592 | 593 | /// 594 | /// You can aggregate search results into groups or buckets by specify 595 | /// one or more `group_by` fields. Separate multiple fields with a comma. 596 | /// To group on a particular field, it must be a faceted field. 597 | /// 598 | [JsonPropertyName("group_by")] 599 | public string GroupBy { get; set; } 600 | 601 | /// 602 | /// Maximum number of hits to be returned for every group. If the `group_limit` is 603 | /// set as `K` then only the top K hits in each group are returned in the response. 604 | /// 605 | [JsonPropertyName("group_limit")] 606 | public int? GroupLimit { get; set; } 607 | 608 | /// 609 | /// Setting this parameter to true will place all documents that have a null value in the group_by field, 610 | /// into a single group. Setting this parameter to false, will cause each document with a null value 611 | /// in the group_by field to not be grouped with other documents. 612 | /// Default: true 613 | /// 614 | [JsonPropertyName("group_missing_values")] 615 | public bool? GroupMissingValues { get; set; } 616 | 617 | public GroupedSearchParameters( 618 | string text, 619 | string queryBy, 620 | string groupBy) : base(text, queryBy) 621 | { 622 | GroupBy = groupBy; 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /src/Typesense/TypesenseClient.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Net.Http.Json; 10 | using System.Net.Mime; 11 | using System.Reflection; 12 | using System.Runtime.CompilerServices; 13 | using System.Security.Cryptography; 14 | using System.Text; 15 | using System.Text.Json; 16 | using System.Text.Json.Serialization; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Typesense.HttpContents; 20 | using Typesense.Setup; 21 | 22 | namespace Typesense; 23 | 24 | public class TypesenseClient : ITypesenseClient 25 | { 26 | private static readonly MediaTypeHeaderValue JsonMediaTypeHeaderValue = MediaTypeHeaderValue.Parse($"{MediaTypeNames.Application.Json};charset={Encoding.UTF8.WebName}"); 27 | private readonly HttpClient _httpClient; 28 | 29 | private readonly JsonSerializerOptions _jsonSerializerDefault = new(); 30 | 31 | private readonly JsonSerializerOptions _jsonNameCaseInsensitiveTrue = new() { PropertyNameCaseInsensitive = true }; 32 | 33 | private readonly JsonSerializerOptions _jsonOptionsCamelCaseIgnoreWritingNull = new() 34 | { 35 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 36 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 37 | }; 38 | 39 | public TypesenseClient(IOptions config, HttpClient httpClient) 40 | { 41 | ArgumentNullException.ThrowIfNull(config); 42 | ArgumentNullException.ThrowIfNull(httpClient); 43 | 44 | var node = config.Value.Nodes.First(); 45 | UriBuilder typeSenseUriBuilder = new UriBuilder(node.Protocol, node.Host, int.Parse(node.Port), node.AdditionalPath); 46 | httpClient.BaseAddress = typeSenseUriBuilder.Uri; 47 | httpClient.DefaultRequestHeaders.Add("X-TYPESENSE-API-KEY", config.Value.ApiKey); 48 | _httpClient = httpClient; 49 | if (config.Value.JsonSerializerOptions is not null) 50 | { 51 | _jsonNameCaseInsensitiveTrue = new JsonSerializerOptions(config.Value.JsonSerializerOptions) 52 | { 53 | PropertyNameCaseInsensitive = true 54 | }; 55 | 56 | _jsonOptionsCamelCaseIgnoreWritingNull = new JsonSerializerOptions(config.Value.JsonSerializerOptions) 57 | { 58 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 59 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 60 | }; 61 | } 62 | } 63 | 64 | public async Task CreateCollection(Schema schema) 65 | { 66 | ArgumentNullException.ThrowIfNull(schema); 67 | 68 | using var jsonContent = JsonContent.Create(schema, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 69 | return await Post("/collections", jsonContent, jsonSerializerOptions: null).ConfigureAwait(false); 70 | } 71 | 72 | public async Task TruncateCollection(string collection) 73 | { 74 | ArgumentNullException.ThrowIfNull(collection); 75 | // The filter_by is a hack because there is a bug in version v28.0 https://github.com/typesense/typesense/pull/2218 76 | return await Delete($"/collections/{collection}/documents?truncate=true&filter_by=", _jsonNameCaseInsensitiveTrue); 77 | } 78 | 79 | public Task CreateDocument(string collection, string document) where T : class 80 | { 81 | return PostDocument(collection, document, upsert: false); 82 | } 83 | 84 | public Task CreateDocument(string collection, T document) where T : class 85 | { 86 | return PostDocument(collection, document, upsert: false); 87 | } 88 | 89 | public Task UpsertDocument(string collection, string document) where T : class 90 | { 91 | return PostDocument(collection, document, upsert: true); 92 | } 93 | 94 | public Task UpsertDocument(string collection, T document) where T : class 95 | { 96 | return PostDocument(collection, document, upsert: true); 97 | } 98 | 99 | private async Task PostDocument(string collection, string document, bool upsert) where T : class 100 | { 101 | if (string.IsNullOrWhiteSpace(collection)) 102 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 103 | if (string.IsNullOrWhiteSpace(document)) 104 | throw new ArgumentException("cannot be null empty or whitespace", nameof(document)); 105 | 106 | using var stringContent = GetApplicationJsonStringContent(document); 107 | return await PostDocuments(collection, stringContent, upsert).ConfigureAwait(false); 108 | } 109 | 110 | private async Task PostDocument(string collection, T document, bool upsert) where T : class 111 | { 112 | ArgumentNullException.ThrowIfNull(document); 113 | if (string.IsNullOrWhiteSpace(collection)) 114 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 115 | 116 | using var jsonContent = JsonContent.Create(document, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 117 | return await PostDocuments(collection, jsonContent, upsert).ConfigureAwait(false); 118 | } 119 | 120 | private Task PostDocuments(string collection, HttpContent httpContent, bool upsert) 121 | { 122 | var path = upsert 123 | ? $"/collections/{collection}/documents?action=upsert" 124 | : $"/collections/{collection}/documents"; 125 | return Post(path, httpContent, _jsonNameCaseInsensitiveTrue); 126 | } 127 | 128 | private Task SearchInternal(string collection, 129 | SearchParameters searchParameters, CancellationToken ctk = default) where TResult : class 130 | { 131 | if (string.IsNullOrWhiteSpace(collection)) 132 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 133 | 134 | ArgumentNullException.ThrowIfNull(searchParameters); 135 | 136 | var parameters = CreateUrlParameters(searchParameters); 137 | return Get($"/collections/{collection}/documents/search?{parameters}", _jsonNameCaseInsensitiveTrue, ctk); 138 | } 139 | 140 | public Task> Search(string collection, SearchParameters searchParameters, CancellationToken ctk = default) 141 | { 142 | return SearchInternal>(collection, searchParameters, ctk); 143 | } 144 | 145 | public Task> SearchGrouped(string collection, GroupedSearchParameters groupedSearchParameters, CancellationToken ctk = default) 146 | { 147 | return SearchInternal>(collection, groupedSearchParameters, ctk); 148 | } 149 | 150 | public async Task>> MultiSearch(ICollection s1, int? limitMultiSearches = null, CancellationToken ctk = default) 151 | { 152 | var searches = new { Searches = s1 }; 153 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 154 | 155 | var path = limitMultiSearches is null 156 | ? "/multi_search" 157 | : $"/multi_search?limit_multi_searches={limitMultiSearches}"; 158 | 159 | var response = await Post(path, json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 160 | 161 | return response.TryGetProperty("results", out var results) 162 | ? results.EnumerateArray().Select(HandleDeserializeMultiSearch).ToList() 163 | : throw new InvalidOperationException("Could not get 'results' property from multi-search response."); 164 | } 165 | 166 | public async Task> MultiSearch(MultiSearchParameters s1, CancellationToken ctk = default) 167 | { 168 | var searches = new { Searches = new MultiSearchParameters[] { s1 } }; 169 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 170 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 171 | 172 | return response.TryGetProperty("results", out var results) 173 | ? HandleDeserializeMultiSearch(results[0]) 174 | : throw new InvalidOperationException("Could not get results from multi-search result."); 175 | } 176 | 177 | public async Task<(MultiSearchResult, MultiSearchResult)> MultiSearch(MultiSearchParameters s1, MultiSearchParameters s2, CancellationToken ctk = default) 178 | { 179 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2 } }; 180 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 181 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 182 | 183 | return response.TryGetProperty("results", out var results) 184 | ? (HandleDeserializeMultiSearch(results[0]), 185 | HandleDeserializeMultiSearch(results[1])) 186 | : throw new InvalidOperationException("Could not get results from multi-search result."); 187 | } 188 | 189 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 190 | MultiSearchParameters s1, 191 | MultiSearchParameters s2, 192 | MultiSearchParameters s3, 193 | CancellationToken ctk = default) 194 | { 195 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3 } }; 196 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 197 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 198 | 199 | return response.TryGetProperty("results", out var results) 200 | ? (HandleDeserializeMultiSearch(results[0]), 201 | HandleDeserializeMultiSearch(results[1]), 202 | HandleDeserializeMultiSearch(results[2])) 203 | : throw new InvalidOperationException("Could not get results from multi-search result."); 204 | } 205 | 206 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 207 | MultiSearchParameters s1, 208 | MultiSearchParameters s2, 209 | MultiSearchParameters s3, 210 | MultiSearchParameters s4, 211 | CancellationToken ctk = default) 212 | { 213 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3, s4 } }; 214 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 215 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 216 | 217 | return (response.TryGetProperty("results", out var results)) 218 | ? (HandleDeserializeMultiSearch(results[0]), 219 | HandleDeserializeMultiSearch(results[1]), 220 | HandleDeserializeMultiSearch(results[2]), 221 | HandleDeserializeMultiSearch(results[3])) 222 | : throw new InvalidOperationException("Could not get results from multi-search result."); 223 | } 224 | 225 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 226 | MultiSearchParameters s1, 227 | MultiSearchParameters s2, 228 | MultiSearchParameters s3, 229 | MultiSearchParameters s4, 230 | MultiSearchParameters s5, 231 | CancellationToken ctk = default) 232 | { 233 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3, s4, s5 } }; 234 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 235 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 236 | 237 | return response.TryGetProperty("results", out var results) 238 | ? (HandleDeserializeMultiSearch(results[0]), 239 | HandleDeserializeMultiSearch(results[1]), 240 | HandleDeserializeMultiSearch(results[2]), 241 | HandleDeserializeMultiSearch(results[3]), 242 | HandleDeserializeMultiSearch(results[4])) 243 | : throw new InvalidOperationException("Could not get results from multi-search result."); 244 | } 245 | 246 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 247 | MultiSearchParameters s1, 248 | MultiSearchParameters s2, 249 | MultiSearchParameters s3, 250 | MultiSearchParameters s4, 251 | MultiSearchParameters s5, 252 | MultiSearchParameters s6, 253 | CancellationToken ctk = default) 254 | { 255 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3, s4, s5, s6 } }; 256 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 257 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 258 | 259 | return response.TryGetProperty("results", out var results) 260 | ? (HandleDeserializeMultiSearch(results[0]), 261 | HandleDeserializeMultiSearch(results[1]), 262 | HandleDeserializeMultiSearch(results[2]), 263 | HandleDeserializeMultiSearch(results[3]), 264 | HandleDeserializeMultiSearch(results[4]), 265 | HandleDeserializeMultiSearch(results[5])) 266 | : throw new InvalidOperationException("Could not get results from multi-search result."); 267 | } 268 | 269 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 270 | MultiSearchParameters s1, 271 | MultiSearchParameters s2, 272 | MultiSearchParameters s3, 273 | MultiSearchParameters s4, 274 | MultiSearchParameters s5, 275 | MultiSearchParameters s6, 276 | MultiSearchParameters s7, 277 | CancellationToken ctk = default) 278 | { 279 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3, s4, s5, s6, s7 } }; 280 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 281 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 282 | 283 | return response.TryGetProperty("results", out var results) 284 | ? (HandleDeserializeMultiSearch(results[0]), 285 | HandleDeserializeMultiSearch(results[1]), 286 | HandleDeserializeMultiSearch(results[2]), 287 | HandleDeserializeMultiSearch(results[3]), 288 | HandleDeserializeMultiSearch(results[4]), 289 | HandleDeserializeMultiSearch(results[5]), 290 | HandleDeserializeMultiSearch(results[6])) 291 | : throw new InvalidOperationException("Could not get results from multi-search result."); 292 | } 293 | 294 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 295 | MultiSearchParameters s1, 296 | MultiSearchParameters s2, 297 | MultiSearchParameters s3, 298 | MultiSearchParameters s4, 299 | MultiSearchParameters s5, 300 | MultiSearchParameters s6, 301 | MultiSearchParameters s7, 302 | MultiSearchParameters s8, 303 | CancellationToken ctk = default) 304 | { 305 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3, s4, s5, s6, s7, s8 } }; 306 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 307 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 308 | 309 | return response.TryGetProperty("results", out var results) 310 | ? (HandleDeserializeMultiSearch(results[0]), 311 | HandleDeserializeMultiSearch(results[1]), 312 | HandleDeserializeMultiSearch(results[2]), 313 | HandleDeserializeMultiSearch(results[3]), 314 | HandleDeserializeMultiSearch(results[4]), 315 | HandleDeserializeMultiSearch(results[5]), 316 | HandleDeserializeMultiSearch(results[6]), 317 | HandleDeserializeMultiSearch(results[7])) 318 | : throw new InvalidOperationException("Could not get results from multi-search result."); 319 | } 320 | 321 | public async Task<(MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult, MultiSearchResult)> MultiSearch( 322 | MultiSearchParameters s1, 323 | MultiSearchParameters s2, 324 | MultiSearchParameters s3, 325 | MultiSearchParameters s4, 326 | MultiSearchParameters s5, 327 | MultiSearchParameters s6, 328 | MultiSearchParameters s7, 329 | MultiSearchParameters s8, 330 | MultiSearchParameters s9, 331 | CancellationToken ctk = default) 332 | { 333 | var searches = new { Searches = new MultiSearchParameters[] { s1, s2, s3, s4, s5, s6, s7, s8, s9 } }; 334 | using var json = JsonContent.Create(searches, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 335 | var response = await Post("/multi_search", json, jsonSerializerOptions: null, ctk).ConfigureAwait(false); 336 | 337 | return response.TryGetProperty("results", out var results) 338 | ? (HandleDeserializeMultiSearch(results[0]), 339 | HandleDeserializeMultiSearch(results[1]), 340 | HandleDeserializeMultiSearch(results[2]), 341 | HandleDeserializeMultiSearch(results[3]), 342 | HandleDeserializeMultiSearch(results[4]), 343 | HandleDeserializeMultiSearch(results[5]), 344 | HandleDeserializeMultiSearch(results[6]), 345 | HandleDeserializeMultiSearch(results[7]), 346 | HandleDeserializeMultiSearch(results[8])) 347 | : throw new InvalidOperationException("Could not get results from multi-search result."); 348 | } 349 | 350 | public Task RetrieveDocument(string collection, string id, CancellationToken ctk = default) where T : class 351 | { 352 | if (string.IsNullOrWhiteSpace(collection)) 353 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 354 | if (string.IsNullOrWhiteSpace(id)) 355 | throw new ArgumentException("cannot be null empty or whitespace", nameof(id)); 356 | 357 | return Get($"/collections/{collection}/documents/{id}", _jsonNameCaseInsensitiveTrue, ctk); 358 | } 359 | 360 | public async Task UpdateDocument(string collection, string id, string document) where T : class 361 | { 362 | if (string.IsNullOrWhiteSpace(collection)) 363 | throw new ArgumentException("Cannot be null empty or whitespace", nameof(collection)); 364 | if (string.IsNullOrWhiteSpace(document)) 365 | throw new ArgumentException("Cannot be null empty or whitespace", nameof(document)); 366 | 367 | using var stringContent = GetApplicationJsonStringContent(document); 368 | return await Patch($"collections/{collection}/documents/{id}", stringContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 369 | } 370 | 371 | public async Task UpdateDocument(string collection, string id, T document) where T : class 372 | { 373 | ArgumentNullException.ThrowIfNull(document); 374 | if (string.IsNullOrWhiteSpace(collection)) 375 | throw new ArgumentException("Cannot be null empty or whitespace", nameof(collection)); 376 | 377 | using var jsonContent = JsonContent.Create(document, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 378 | return await Patch($"collections/{collection}/documents/{id}", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 379 | } 380 | 381 | public Task RetrieveCollection(string name, CancellationToken ctk = default) 382 | { 383 | if (string.IsNullOrWhiteSpace(name)) 384 | throw new ArgumentException("cannot be null empty or whitespace", nameof(name)); 385 | 386 | return Get($"/collections/{name}", jsonSerializerOptions: null, ctk); 387 | } 388 | 389 | public Task> RetrieveCollections(CancellationToken ctk = default) 390 | { 391 | return Get>("/collections", jsonSerializerOptions: null, ctk); 392 | } 393 | 394 | public Task DeleteDocument(string collection, string documentId) where T : class 395 | { 396 | if (string.IsNullOrWhiteSpace(collection)) 397 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 398 | if (string.IsNullOrWhiteSpace(documentId)) 399 | throw new ArgumentException("cannot be null empty or whitespace", nameof(documentId)); 400 | 401 | return Delete($"/collections/{collection}/documents/{documentId}", _jsonNameCaseInsensitiveTrue); 402 | } 403 | 404 | public Task DeleteDocuments(string collection, string filter, int batchSize = 40) 405 | { 406 | if (string.IsNullOrWhiteSpace(collection)) 407 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 408 | if (string.IsNullOrWhiteSpace(filter)) 409 | throw new ArgumentException("cannot be null empty or whitespace", nameof(filter)); 410 | if (batchSize < 0) 411 | throw new ArgumentException("has to be greater than 0", nameof(batchSize)); 412 | 413 | return Delete($"/collections/{collection}/documents?filter_by={Uri.EscapeDataString(filter)}&batch_size={batchSize}", _jsonNameCaseInsensitiveTrue); 414 | } 415 | 416 | public Task DeleteCollection(string name, bool compactStore = true) 417 | { 418 | if (string.IsNullOrWhiteSpace(name)) 419 | throw new ArgumentException("cannot be null empty or whitespace", nameof(name)); 420 | 421 | // add compact_store query parameter only on false since it is true by default 422 | var compactStoreQuery = compactStore ? string.Empty : "?compact_store=false"; 423 | 424 | return Delete($"/collections/{name}{compactStoreQuery}", _jsonNameCaseInsensitiveTrue); 425 | } 426 | 427 | public async Task UpdateCollection( 428 | string name, 429 | UpdateSchema updateSchema) 430 | { 431 | if (string.IsNullOrWhiteSpace(name)) 432 | throw new ArgumentException("cannot be null empty or whitespace", nameof(name)); 433 | 434 | using var jsonContent = JsonContent.Create( 435 | updateSchema, 436 | JsonMediaTypeHeaderValue, 437 | _jsonOptionsCamelCaseIgnoreWritingNull); 438 | 439 | return await Patch($"/collections/{name}", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 440 | } 441 | 442 | public async Task UpdateDocuments(string collection, T document, string filter, bool fullUpdate = false) 443 | { 444 | if (string.IsNullOrWhiteSpace(collection)) 445 | throw new ArgumentException("cannot be null empty or whitespace", nameof(collection)); 446 | if (document == null) 447 | throw new ArgumentNullException(nameof(document), "cannot be null"); 448 | if (string.IsNullOrWhiteSpace(filter)) 449 | throw new ArgumentException("cannot be null empty or whitespace", nameof(filter)); 450 | 451 | using var jsonContent = JsonContent.Create(document, JsonMediaTypeHeaderValue, fullUpdate ? _jsonSerializerDefault : _jsonOptionsCamelCaseIgnoreWritingNull); 452 | 453 | return await Patch($"collections/{collection}/documents?filter_by={Uri.EscapeDataString(filter)}&action=update", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 454 | } 455 | 456 | public async Task> ImportDocuments( 457 | string collection, 458 | string documents, 459 | int batchSize = 40, 460 | ImportType importType = ImportType.Create, 461 | int? remoteEmbeddingBatchSize = null, 462 | bool? returnId = null) 463 | { 464 | if (string.IsNullOrWhiteSpace(documents)) 465 | throw new ArgumentException("cannot be null empty or whitespace", nameof(documents)); 466 | using var stringContent = GetTextPlainStringContent(documents); 467 | return await ImportDocuments(collection, stringContent, batchSize, importType, remoteEmbeddingBatchSize, returnId).ConfigureAwait(false); 468 | } 469 | 470 | private async Task> ImportDocuments( 471 | string collection, 472 | HttpContent documents, 473 | int batchSize = 40, 474 | ImportType importType = ImportType.Create, 475 | int? remoteEmbeddingBatchSize = null, 476 | bool? returnId = null) 477 | { 478 | ArgumentNullException.ThrowIfNull(documents); 479 | if (string.IsNullOrWhiteSpace(collection)) 480 | throw new ArgumentException("cannot be null or whitespace", nameof(collection)); 481 | 482 | var path = $"/collections/{collection}/documents/import?batch_size={batchSize}"; 483 | 484 | if (remoteEmbeddingBatchSize.HasValue) 485 | path += $"&remote_embedding_batch_size={remoteEmbeddingBatchSize.Value}"; 486 | 487 | path += importType switch 488 | { 489 | ImportType.Create => "&action=create", 490 | ImportType.Update => "&action=update", 491 | ImportType.Upsert => "&action=upsert", 492 | ImportType.Emplace => "&action=emplace", 493 | _ => throw new ArgumentException($"Could not handle {nameof(ImportType)} with name '{Enum.GetName(importType)}'", nameof(importType)), 494 | }; 495 | 496 | if (returnId is not null) 497 | { 498 | path += $"&return_id={returnId}"; 499 | } 500 | 501 | using var response = await _httpClient.PostAsync(path, documents).ConfigureAwait(false); 502 | if (!response.IsSuccessStatusCode) 503 | await GetException(response, CancellationToken.None).ConfigureAwait(false); 504 | 505 | List result = new(); 506 | await foreach (var line in GetLines(response).ConfigureAwait(false)) 507 | { 508 | var importResponse = JsonSerializer.Deserialize(line) ?? throw new ArgumentException("Null is not valid for documents."); 509 | result.Add(importResponse); 510 | } 511 | return result; 512 | } 513 | 514 | public async Task> ImportDocuments( 515 | string collection, 516 | IEnumerable documents, 517 | int batchSize = 40, 518 | ImportType importType = ImportType.Create, 519 | int? remoteEmbeddingBatchSize = null, 520 | bool? returnId = null) 521 | { 522 | ArgumentNullException.ThrowIfNull(documents); 523 | 524 | using var streamLinesContent = new StreamStringLinesHttpContent(documents); 525 | return await ImportDocuments(collection, streamLinesContent, batchSize, importType, remoteEmbeddingBatchSize).ConfigureAwait(false); 526 | } 527 | 528 | public async Task> ImportDocuments( 529 | string collection, 530 | IEnumerable documents, 531 | int batchSize = 40, 532 | ImportType importType = ImportType.Create, 533 | int? remoteEmbeddingBatchSize = null, 534 | bool? returnId = null) 535 | { 536 | ArgumentNullException.ThrowIfNull(documents); 537 | 538 | using var streamJsonLinesContent = new StreamJsonLinesHttpContent(documents, _jsonOptionsCamelCaseIgnoreWritingNull); 539 | return await ImportDocuments(collection, streamJsonLinesContent, batchSize, importType, remoteEmbeddingBatchSize).ConfigureAwait(false); 540 | } 541 | 542 | public async Task> ImportDocuments( 543 | string collection, 544 | Stream stream, 545 | int batchSize = 40, 546 | ImportType importType = ImportType.Create, 547 | int? remoteEmbeddingBatchSize = null, 548 | bool? returnId = null) 549 | { 550 | ArgumentNullException.ThrowIfNull(stream); 551 | 552 | using var streamContent = new StreamContent(stream); 553 | return await ImportDocuments(collection, streamContent, batchSize, importType, remoteEmbeddingBatchSize).ConfigureAwait(false); 554 | } 555 | 556 | public Task> ExportDocuments(string collection, CancellationToken ctk = default) 557 | { 558 | return ExportDocuments(collection, new ExportParameters(), ctk); 559 | } 560 | 561 | public async Task> ExportDocuments(string collection, ExportParameters exportParameters, CancellationToken ctk = default) 562 | { 563 | if (string.IsNullOrWhiteSpace(collection)) 564 | throw new ArgumentException("cannot be null or whitespace.", nameof(collection)); 565 | 566 | ArgumentNullException.ThrowIfNull(exportParameters); 567 | 568 | var parameters = CreateUrlParameters(exportParameters); 569 | var lines = GetLines($"/collections/{collection}/documents/export?{parameters}", ctk).ConfigureAwait(false); 570 | List documents = new(); 571 | await foreach (var line in lines) 572 | { 573 | if (string.IsNullOrWhiteSpace(line)) 574 | continue; 575 | documents.Add(JsonSerializer.Deserialize(line, _jsonNameCaseInsensitiveTrue) ?? 576 | throw new ArgumentException("Null is not valid for documents")); 577 | } 578 | return documents; 579 | } 580 | 581 | public async Task CreateKey(Key key) 582 | { 583 | ArgumentNullException.ThrowIfNull(key); 584 | 585 | using var jsonContent = JsonContent.Create(key, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 586 | return await Post("/keys", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 587 | } 588 | 589 | public Task RetrieveKey(int id, CancellationToken ctk = default) 590 | { 591 | return Get($"/keys/{id}", _jsonNameCaseInsensitiveTrue, ctk); 592 | } 593 | 594 | public Task DeleteKey(int id) 595 | { 596 | return Delete($"/keys/{id}", _jsonNameCaseInsensitiveTrue); 597 | } 598 | 599 | public Task ListKeys(CancellationToken ctk = default) 600 | { 601 | return Get($"/keys", _jsonNameCaseInsensitiveTrue, ctk); 602 | } 603 | 604 | public string GenerateScopedSearchKey(string securityKey, string parameters) 605 | { 606 | if (String.IsNullOrWhiteSpace(securityKey)) 607 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(securityKey)); 608 | if (String.IsNullOrWhiteSpace(parameters)) 609 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(parameters)); 610 | 611 | var securityKeyAsBuffer = Encoding.UTF8.GetBytes(securityKey); 612 | var parametersAsBuffer = Encoding.UTF8.GetBytes(parameters); 613 | 614 | using var hmac = new HMACSHA256(securityKeyAsBuffer); 615 | var hash = hmac.ComputeHash(parametersAsBuffer); 616 | var digest = Convert.ToBase64String(hash); 617 | var keyPrefix = securityKey[..4]; 618 | var rawScopedKey = $"{digest}{keyPrefix}{parameters}"; 619 | 620 | return Convert.ToBase64String(Encoding.UTF8.GetBytes(rawScopedKey)); 621 | } 622 | 623 | public async Task UpsertSearchOverride( 624 | string collection, string overrideName, SearchOverride searchOverride) 625 | { 626 | if (string.IsNullOrWhiteSpace(collection)) 627 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(collection)); 628 | if (string.IsNullOrWhiteSpace(overrideName)) 629 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(overrideName)); 630 | 631 | ArgumentNullException.ThrowIfNull(searchOverride); 632 | 633 | using var jsonContent = JsonContent.Create(searchOverride, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 634 | return await Put($"/collections/{collection}/overrides/{overrideName}", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 635 | } 636 | 637 | public Task ListSearchOverrides(string collection, CancellationToken ctk = default) 638 | { 639 | if (string.IsNullOrWhiteSpace(collection)) 640 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(collection)); 641 | 642 | return Get($"collections/{collection}/overrides", _jsonNameCaseInsensitiveTrue, ctk); 643 | } 644 | 645 | public Task RetrieveSearchOverride(string collection, string overrideName, CancellationToken ctk = default) 646 | { 647 | if (string.IsNullOrWhiteSpace(collection)) 648 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(collection)); 649 | if (string.IsNullOrWhiteSpace(overrideName)) 650 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(overrideName)); 651 | 652 | return Get($"/collections/{collection}/overrides/{overrideName}", _jsonNameCaseInsensitiveTrue, ctk); 653 | } 654 | 655 | public Task DeleteSearchOverride( 656 | string collection, string overrideName) 657 | { 658 | if (string.IsNullOrWhiteSpace(collection)) 659 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(collection)); 660 | if (string.IsNullOrWhiteSpace(overrideName)) 661 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(overrideName)); 662 | 663 | return Delete($"/collections/{collection}/overrides/{overrideName}", _jsonNameCaseInsensitiveTrue); 664 | } 665 | 666 | public async Task UpsertCollectionAlias(string aliasName, CollectionAlias collectionAlias) 667 | { 668 | if (string.IsNullOrWhiteSpace(aliasName)) 669 | throw new ArgumentException("cannot be null, empty or whitespace.", nameof(aliasName)); 670 | 671 | ArgumentNullException.ThrowIfNull(collectionAlias); 672 | 673 | using var jsonContent = JsonContent.Create(collectionAlias, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 674 | return await Put($"/aliases/{aliasName}", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 675 | } 676 | 677 | public Task RetrieveCollectionAlias(string collection, CancellationToken ctk = default) 678 | { 679 | if (string.IsNullOrWhiteSpace(collection)) 680 | throw new ArgumentException("cannot be null or whitespace.", nameof(collection)); 681 | 682 | return Get($"/aliases/{collection}", _jsonNameCaseInsensitiveTrue, ctk); 683 | } 684 | 685 | public Task ListCollectionAliases(CancellationToken ctk = default) 686 | { 687 | return Get("/aliases", _jsonNameCaseInsensitiveTrue, ctk); 688 | } 689 | 690 | public Task DeleteCollectionAlias(string aliasName) 691 | { 692 | if (string.IsNullOrWhiteSpace(aliasName)) 693 | throw new ArgumentException("cannot be null or whitespace.", nameof(aliasName)); 694 | 695 | return Delete($"/aliases/{aliasName}", _jsonNameCaseInsensitiveTrue); 696 | } 697 | 698 | public async Task UpsertSynonym( 699 | string collection, string synonym, SynonymSchema schema) 700 | { 701 | if (string.IsNullOrWhiteSpace(collection)) 702 | throw new ArgumentException("cannot be null or whitespace.", nameof(collection)); 703 | if (string.IsNullOrWhiteSpace(synonym)) 704 | throw new ArgumentException("cannot be null or whitespace.", nameof(synonym)); 705 | 706 | ArgumentNullException.ThrowIfNull(schema); 707 | 708 | using var jsonContent = JsonContent.Create(schema, JsonMediaTypeHeaderValue, _jsonOptionsCamelCaseIgnoreWritingNull); 709 | return await Put($"/collections/{collection}/synonyms/{synonym}", jsonContent, _jsonNameCaseInsensitiveTrue).ConfigureAwait(false); 710 | } 711 | 712 | public Task RetrieveSynonym(string collection, string synonym, CancellationToken ctk = default) 713 | { 714 | if (string.IsNullOrWhiteSpace(collection)) 715 | throw new ArgumentException($"{nameof(collection)} cannot be null, empty or whitespace."); 716 | if (string.IsNullOrWhiteSpace(synonym)) 717 | throw new ArgumentException($"{nameof(synonym)} cannot be null, empty or whitespace."); 718 | 719 | return Get($"/collections/{collection}/synonyms/{synonym}", _jsonNameCaseInsensitiveTrue, ctk); 720 | } 721 | 722 | public Task ListSynonyms(string collection, CancellationToken ctk = default) 723 | { 724 | if (string.IsNullOrWhiteSpace(collection)) 725 | throw new ArgumentException($"{nameof(collection)} cannot be null, empty or whitespace."); 726 | 727 | return Get($"/collections/{collection}/synonyms", _jsonNameCaseInsensitiveTrue, ctk); 728 | } 729 | 730 | public Task DeleteSynonym(string collection, string synonym) 731 | { 732 | if (string.IsNullOrWhiteSpace(collection)) 733 | throw new ArgumentException($"{nameof(collection)} cannot be null, empty or whitespace."); 734 | if (string.IsNullOrWhiteSpace(synonym)) 735 | throw new ArgumentException($"{nameof(synonym)} cannot be null, empty or whitespace."); 736 | 737 | return Delete($"/collections/{collection}/synonyms/{synonym}", _jsonNameCaseInsensitiveTrue); 738 | } 739 | 740 | public Task RetrieveMetrics(CancellationToken ctk = default) 741 | { 742 | return Get("/metrics.json", jsonSerializerOptions: null, ctk); 743 | } 744 | 745 | public Task RetrieveStats(CancellationToken ctk = default) 746 | { 747 | return Get("/stats.json", jsonSerializerOptions: null, ctk); 748 | } 749 | 750 | public Task RetrieveHealth(CancellationToken ctk = default) 751 | { 752 | return Get("/health", jsonSerializerOptions: null, ctk); 753 | } 754 | 755 | public Task CreateSnapshot(string snapshotPath, CancellationToken ctk = default) 756 | { 757 | if (string.IsNullOrWhiteSpace(snapshotPath)) 758 | throw new ArgumentException( 759 | "The snapshot path must not be null, empty or consist of whitespace characters only.", 760 | nameof(snapshotPath)); 761 | 762 | return Post($"/operations/snapshot?snapshot_path={Uri.EscapeDataString(snapshotPath)}", httpContent: null, _jsonNameCaseInsensitiveTrue, ctk); 763 | } 764 | 765 | public Task CompactDisk(CancellationToken ctk = default) 766 | { 767 | return Post("/operations/db/compact", httpContent: null, _jsonNameCaseInsensitiveTrue, ctk); 768 | } 769 | 770 | private static string CreateUrlParameters(T queryParameters) 771 | where T : notnull 772 | { 773 | // Add all non-null properties to the query 774 | var parameters = queryParameters.GetType() 775 | .GetProperties() 776 | .Select(prop => 777 | { 778 | var value = prop.GetValue(queryParameters); 779 | 780 | var stringValue = value switch 781 | { 782 | null => null, 783 | true => "true", 784 | false => "false", 785 | Enum e => e.ToString().ToLowerInvariant(), 786 | _ => value.ToString(), 787 | }; 788 | 789 | return new 790 | { 791 | Key = prop.GetCustomAttribute()?.Name, 792 | Value = stringValue, 793 | }; 794 | }) 795 | .Where(parameter => parameter.Value != null && parameter.Key != null); 796 | 797 | return string.Join("", parameters.Select(p => $"&{Uri.EscapeDataString(p.Key!)}={Uri.EscapeDataString(p.Value!)}")); 798 | } 799 | 800 | private async Task Get(string path, JsonSerializerOptions? jsonSerializerOptions, CancellationToken ctk = default) 801 | { 802 | using var response = await _httpClient.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ctk).ConfigureAwait(false); 803 | if (!response.IsSuccessStatusCode) 804 | await GetException(response, ctk).ConfigureAwait(false); 805 | 806 | return await ReadJsonFromResponseMessage(jsonSerializerOptions, response, ctk).ConfigureAwait(false); 807 | } 808 | 809 | private async IAsyncEnumerable GetLines(string path, [EnumeratorCancellation] CancellationToken ctk = default) 810 | { 811 | using var response = await _httpClient.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ctk).ConfigureAwait(false); 812 | if (!response.IsSuccessStatusCode) 813 | await GetException(response, ctk).ConfigureAwait(false); 814 | 815 | await foreach (var p in GetLines(response, ctk).ConfigureAwait(false)) 816 | yield return p; 817 | } 818 | 819 | private static async IAsyncEnumerable GetLines(HttpResponseMessage response, [EnumeratorCancellation] CancellationToken ctk = default) 820 | { 821 | await using var stream = await response.Content.ReadAsStreamAsync(ctk).ConfigureAwait(false); 822 | using StreamReader streamReader = new(stream); 823 | 824 | while (true) 825 | { 826 | var line = await streamReader.ReadLineAsync().ConfigureAwait(false); 827 | if (line is null) 828 | break; 829 | yield return line; 830 | } 831 | } 832 | 833 | private async Task Delete(string path, JsonSerializerOptions? jsonSerializerOptions, CancellationToken ctk = default) 834 | { 835 | using var response = await _httpClient.DeleteAsync(path, ctk).ConfigureAwait(false); 836 | if (!response.IsSuccessStatusCode) 837 | await GetException(response, ctk).ConfigureAwait(false); 838 | 839 | return await ReadJsonFromResponseMessage(jsonSerializerOptions, response, ctk).ConfigureAwait(false); 840 | } 841 | 842 | private async Task Post(string path, HttpContent? httpContent, JsonSerializerOptions? jsonSerializerOptions, CancellationToken ctk = default) 843 | { 844 | using var response = await _httpClient.PostAsync(path, httpContent, ctk).ConfigureAwait(false); 845 | if (!response.IsSuccessStatusCode) 846 | await GetException(response, ctk).ConfigureAwait(false); 847 | 848 | return await ReadJsonFromResponseMessage(jsonSerializerOptions, response, ctk).ConfigureAwait(false); 849 | } 850 | 851 | private async Task Patch(string path, HttpContent? httpContent, JsonSerializerOptions? jsonSerializerOptions, CancellationToken ctk = default) 852 | { 853 | using var response = await _httpClient.PatchAsync(path, httpContent, ctk).ConfigureAwait(false); 854 | if (!response.IsSuccessStatusCode) 855 | await GetException(response, ctk).ConfigureAwait(false); 856 | 857 | return await ReadJsonFromResponseMessage(jsonSerializerOptions, response, ctk).ConfigureAwait(false); 858 | } 859 | 860 | private async Task Put(string path, HttpContent? httpContent, JsonSerializerOptions? jsonSerializerOptions, CancellationToken ctk = default) 861 | { 862 | using var response = await _httpClient.PutAsync(path, httpContent, ctk).ConfigureAwait(false); 863 | if (!response.IsSuccessStatusCode) 864 | await GetException(response, ctk).ConfigureAwait(false); 865 | 866 | return await ReadJsonFromResponseMessage(jsonSerializerOptions, response, ctk).ConfigureAwait(false); 867 | } 868 | 869 | private static async Task ReadJsonFromResponseMessage(JsonSerializerOptions? jsonSerializerOptions, HttpResponseMessage response, CancellationToken ctk) 870 | { 871 | return await response.Content.ReadFromJsonAsync(jsonSerializerOptions, ctk).ConfigureAwait(false) ?? throw new ArgumentException("Deserialize is not allowed to return null."); 872 | } 873 | 874 | private static async Task GetException(HttpResponseMessage response, CancellationToken ctk) 875 | { 876 | var message = await response.Content.ReadAsStringAsync(ctk).ConfigureAwait(false); 877 | throw GetException(response.StatusCode, message); 878 | } 879 | 880 | private static TypesenseApiException GetException(HttpStatusCode statusCode, string message) 881 | => statusCode switch 882 | { 883 | HttpStatusCode.BadRequest => new TypesenseApiBadRequestException(message), 884 | HttpStatusCode.Unauthorized => new TypesenseApiUnauthorizedException(message), 885 | HttpStatusCode.NotFound => new TypesenseApiNotFoundException(message), 886 | HttpStatusCode.Conflict => new TypesenseApiConflictException(message), 887 | HttpStatusCode.UnprocessableEntity => new TypesenseApiUnprocessableEntityException(message), 888 | HttpStatusCode.ServiceUnavailable => new TypesenseApiUnprocessableEntityException(message), 889 | // If we receive a status code that has not been documented, we throw TypesenseApiException. 890 | _ => throw new TypesenseApiException($"Received an unspecified status-code: '{Enum.GetName(statusCode)}' from Typesense, with message: '{message}'.") 891 | }; 892 | 893 | private static StringContent GetApplicationJsonStringContent(string jsonString) 894 | => new(jsonString, Encoding.UTF8, MediaTypeNames.Application.Json); 895 | 896 | private static StringContent GetTextPlainStringContent(string jsonString) 897 | => new(jsonString, Encoding.UTF8, "text/plain"); 898 | 899 | private MultiSearchResult HandleDeserializeMultiSearch(JsonElement jsonElement) 900 | => jsonElement.Deserialize>(_jsonNameCaseInsensitiveTrue) 901 | ?? throw new InvalidOperationException($"Could not deserialize {typeof(T)}, Received following from Typesense: '{jsonElement}'."); 902 | } 903 | --------------------------------------------------------------------------------