ZeroLevel.Qdrant library

pull/1/head
Ogoun 3 years ago
parent ba83fcf848
commit 61461778e4

@ -0,0 +1,10 @@
namespace ZeroLevel.Qdrant.DataAttributes
{
/// <summary>
/// Attribute for directly specifying the 'float' data type of qdrant
/// </summary>
public class FloatAttribute
: QdrantAttribute
{
}
}

@ -0,0 +1,10 @@
namespace ZeroLevel.Qdrant.DataAttributes
{
/// <summary>
/// Attribute for directly specifying the 'geo' data type of qdrant
/// </summary>
public class GeoAttribute
: QdrantAttribute
{
}
}

@ -0,0 +1,10 @@
namespace ZeroLevel.Qdrant.DataAttributes
{
/// <summary>
/// Attribute for directly specifying the 'integer' data type of qdrant
/// </summary>
public class IntegerAttribute
: QdrantAttribute
{
}
}

@ -0,0 +1,10 @@
namespace ZeroLevel.Qdrant.DataAttributes
{
/// <summary>
/// Attribute for directly specifying the 'keyword' data type of qdrant
/// </summary>
public class KeywordAttribute
: QdrantAttribute
{
}
}

@ -0,0 +1,12 @@
using System;
namespace ZeroLevel.Qdrant.DataAttributes
{
/// <summary>
/// Attribute for directly specifying the data type of qdrant
/// </summary>
public abstract class QdrantAttribute
: Attribute
{
}
}

@ -0,0 +1,10 @@
namespace ZeroLevel.Qdrant
{
public static class DoubleExtensions
{
public static string ConvertToString(this double num)
{
return num.ToString().Replace(',', '.');
}
}
}

@ -0,0 +1,81 @@
namespace ZeroLevel.Qdrant.Models.Filters
{
/// <summary>
/// Condition for qdrant filters
/// </summary>
public class Condition
: Operand
{
/*
integer - 64-bit integer in the range -9223372036854775808 to 9223372036854775807. array of long
float - 64-bit floating point number. array of double
keyword - string value. array of strings
geo - Geographical coordinates. Example: { "lon": 52.5200, "lat": 13.4050 } array of lon&lat of double
*/
public string Json { get; set; }
public static Condition Ids(long[] values)
{
return new Condition
{
Json = $"{{ \"has_id\": [{string.Join(",", values)}] }}"
};
}
public static Condition IntegerMatch(string name, long value)
{
return new Condition
{
Json = $"{{ \"key\": \"{name}\", \"match\": {{ \"integer\": \"{value}\" }} }}"
};
}
public static Condition IntegerRange(string name, long left, long rigth, bool include_left, bool include_right)
{
var left_cond = include_left ? $"\"lt\": null,\"lte\": {left}" : $"\"lt\": {left},\"lte\": null";
var right_cond = include_right ? $"\"gt\": null,\"gte\": {rigth}" : $"\"gt\": {rigth},\"gte\": null";
return new Condition
{
Json = $"{{ \"key\": \"{name}\", \"range\": {{ {right_cond}, {left_cond} }} }}"
};
}
public static Condition FloatRange(string name, double left, double rigth, bool include_left, bool include_right)
{
var left_cond = include_left ? $"\"lt\": null,\"lte\": {left.ConvertToString()}" : $"\"lt\": {left.ConvertToString()},\"lte\": null";
var right_cond = include_right ? $"\"gt\": null,\"gte\": {rigth.ConvertToString()}" : $"\"gt\": {rigth.ConvertToString()},\"gte\": null";
return new Condition
{
Json = $"{{ \"key\": \"{name}\", \"range\": {{ {left_cond}, {right_cond} }} }}"
};
}
public static Condition KeywordMatch(string name, string value)
{
return new Condition
{
Json = $"{{ \"key\": \"{name}\", \"match\": {{ \"keyword\": \"{value}\" }} }}"
};
}
public static Condition GeoBox(string name, Location top_left, Location bottom_right)
{
return new Condition
{
Json = $"{{ \"key\": \"{name}\", \"geo_bounding_box\": {{ \"bottom_right\": {{ \"lat\": {bottom_right.lat.ConvertToString()}, \"lon\": {bottom_right.lon.ConvertToString()} }}, \"top_left\": {{ \"lat\": {top_left.lat.ConvertToString()}, \"lon\": {top_left.lon.ConvertToString()} }} }} }}"
};
}
public static Condition GeoRadius(string name, Location location, double radius)
{
return new Condition
{
Json = $"{{\"key\": \"{name}\", \"geo_radius\": {{\"center\": {{ \"lat\": {location.lat.ConvertToString()}, \"lon\": {location.lon.ConvertToString()} }}, \"radius\": {radius.ConvertToString()} }} }}"
};
}
public override string ToJSON()
{
return Json;
}
}
}

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ZeroLevel.Qdrant.Models.Filters
{
/// <summary>
/// Filter for search in qdrant
/// </summary>
public class Filter
{
private List<Group> _groups = new List<Group>();
public bool IsEmpty => _groups?.Count == 0;
public Group AppendGroup(GroupOperator op)
{
var g = new Group(op);
_groups.Add(g);
return g;
}
public string ToJSON()
{
var json = new StringBuilder();
json.Append("\"filter\": {");
json.Append(string.Join(",", _groups.Select(g => g.ToJSON())));
json.Append("}");
return json.ToString();
}
}
}

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
namespace ZeroLevel.Qdrant.Models.Filters
{
public class Group
: Operand
{
private List<Operand> _items = new List<Operand>();
public GroupOperator Operator { get; private set; }
public Group(GroupOperator op)
{
Operator = op;
}
public Group AppendGroup(GroupOperator op)
{
var g = new Group(op);
_items.Add(g);
return g;
}
public Group AppendCondition(Condition condition)
{
_items.Add(condition);
return this;
}
public override string ToJSON()
{
string op;
switch (Operator)
{
case GroupOperator.MustNot: op = "must_not"; break;
case GroupOperator.Must: op = "must"; break;
default: op = "mushould"; break;
}
return $"\"{op}\": [{string.Join(",", _items.Select(i => i.ToJSON()))}]";
}
}
}

@ -0,0 +1,9 @@
namespace ZeroLevel.Qdrant.Models.Filters
{
public enum GroupOperator
{
Must,
Should,
MustNot
}
}

@ -0,0 +1,7 @@
namespace ZeroLevel.Qdrant.Models.Filters
{
public abstract class Operand
{
public abstract string ToJSON();
}
}

@ -0,0 +1,8 @@
namespace ZeroLevel.Qdrant.Models
{
public class Location
{
public double lon { get; set; }
public double lat { get; set; }
}
}

@ -0,0 +1,23 @@
namespace ZeroLevel.Qdrant.Models.Requests
{
internal sealed class CreateCollectionParameters
{
public string name { get; set; }
public string distance { get; set; }
public int vector_size { get; set; }
}
internal sealed class CreateCollectionReqeust
{
public CreateCollectionParameters create_collection { get; set; }
public CreateCollectionReqeust(string name, string distance, int vector_size)
{
create_collection = new CreateCollectionParameters
{
name = name,
distance = distance,
vector_size = vector_size
};
}
}
}

@ -0,0 +1,8 @@
namespace ZeroLevel.Qdrant.Models.Requests
{
internal sealed class CreateIndexRequest
{
public CreateIndexRequest(string name) => create_index = name;
public string create_index { get; set; }
}
}

@ -0,0 +1,8 @@
namespace ZeroLevel.Qdrant.Models.Responces
{
internal sealed class DeleteCollectionRequest
{
public DeleteCollectionRequest(string name) => delete_collection = name;
public string delete_collection { get; set; }
}
}

@ -0,0 +1,11 @@
namespace ZeroLevel.Qdrant.Models.Requests
{
internal sealed class DeletePoints
{
public long[] ids { get; set; }
}
internal sealed class DeletePointsRequest
{
public DeletePoints delete_points { get; set; }
}
}

@ -0,0 +1,7 @@
namespace ZeroLevel.Qdrant.Models.Requests
{
internal sealed class PointsRequest
{
public long[] ids { get; set; }
}
}

@ -0,0 +1,88 @@
using System.Linq;
using System.Text;
using ZeroLevel.Qdrant.Services;
using ZeroLevel.Services.Collections;
namespace ZeroLevel.Qdrant.Models.Requests
{
/*
integer - 64-bit integer in the range -9223372036854775808 to 9223372036854775807. array of long
float - 64-bit floating point number. array of double
keyword - string value. array of strings
geo - Geographical coordinates. Example: { "lon": 52.5200, "lat": 13.4050 } array of lon&lat of double
*/
public sealed class UpsertPoint<T>
{
public long? id { get; set; } = null;
public T payload { get; set; }
public double[] vector { get; set; }
}
public sealed class UpsertPoints<T>
{
public UpsertPoint<T>[] points { get; set; }
}
public sealed class PointsUploadRequest<T>
{
private static IEverythingStorage _cachee = EverythingStorage.Create();
public UpsertPoints<T> upsert_points { get; set; }
public string ToJSON()
{
if (!_cachee.ContainsKey<QdrantJsonConverter<T>>("converter"))
{
_cachee.Add("converter", new QdrantJsonConverter<T>());
}
var converter = _cachee.Get<QdrantJsonConverter<T>>("converter");
var json = new StringBuilder();
json.Append("{");
json.Append("\"upsert_points\": {");
json.Append("\"points\":[ {");
json.Append(string.Join("},{", upsert_points.points.Select(p => $"\"id\": {p.id}, \"payload\": {{ {converter.ToJson(p.payload)} }}, \"vector\": [{ string.Join(",", p.vector.Select(f => f.ConvertToString()))}]")));
json.Append("}]");
json.Append("}");
json.Append("}");
return json.ToString();
}
}
public sealed class ColumnPoints<T>
{
public long[] ids { get; set; }
public T[] payloads { get; set; }
public double[,] vectors { get; set; }
}
public sealed class UpsertColumnPoints<T>
{
public ColumnPoints<T> batch { get; set; }
}
public sealed class PointsColumnUploadRequest<T>
{
private static IEverythingStorage _cachee = EverythingStorage.Create();
public UpsertColumnPoints<T> upsert_points { get; set; }
public string ToJSON()
{
if (!_cachee.ContainsKey<QdrantJsonConverter<T>>("converter"))
{
_cachee.Add("converter", new QdrantJsonConverter<T>());
}
var converter = _cachee.Get<QdrantJsonConverter<T>>("converter");
var json = new StringBuilder();
json.Append("{");
json.Append("\"upsert_points\": {");
json.Append("\"batch\": {");
json.Append($"\"ids\": [{string.Join(",", upsert_points.batch.ids)}], ");
json.Append($"\"payloads\": [ {{ {string.Join("} ,{ ", upsert_points.batch.payloads.Select(payload => converter.ToJson(payload)))} }} ], ");
json.Append($"\"vectors\": [{string.Join(",", Enumerable.Range(0, upsert_points.batch.vectors.GetLength(0)).Select(row => "[" + string.Join(",", ArrayExtensions.GetRow(upsert_points.batch.vectors, row).Select(f => f.ConvertToString())) + "]"))}]");
json.Append("}");
json.Append("}");
json.Append("}");
return json.ToString();
}
}
}

@ -0,0 +1,84 @@
using ZeroLevel.Qdrant.Models.Filters;
using System;
using System.Linq;
using System.Text;
namespace ZeroLevel.Qdrant.Models.Requests
{
internal sealed class SearchRequest
{
/// <summary>
/// Look only for points which satisfies this conditions
/// </summary>
public Filter Filter { get; set; }
/// <summary>
/// Look for vectors closest to this
/// </summary>
public double[] FloatVector { get; set; }
public long[] IntegerVector { get; set; }
/// <summary>
/// Max number of result to return
/// </summary>
public uint Top { get; set; }
/// <summary>
/// Params relevant to HNSW index /// Size of the beam in a beam-search. Larger the value - more accurate the result, more time required for search.
/// </summary>
public uint? HNSW { get; set; } = null;
/*
{
"filter": {
"must": [
{
"key": "city",
"match": {
"keyword": "London"
}
}
]
},
"params": {
"hnsw_ef": 128
},
"vector": [0.2, 0.1, 0.9, 0.7],
"top": 3
}
*/
public string ToJson()
{
var json = new StringBuilder();
json.Append("{");
if (Filter == null || Filter.IsEmpty)
{
json.Append("\"filter\": null,");
}
else
{
json.Append(Filter.ToJSON());
json.Append(',');
}
if (HNSW != null)
{
json.Append($"\"params\": {{ \"hnsw_ef\": {HNSW.Value} }},");
}
if (FloatVector != null)
{
json.Append($"\"vector\": [{string.Join(",", FloatVector.Select(f => f.ConvertToString()))}],");
}
else if (IntegerVector != null)
{
json.Append($"\"vector\": [{string.Join(",", IntegerVector)}],");
}
else
{
throw new ArgumentException("No one vectors is set");
}
json.Append($"\"top\": {Top}");
json.Append("}");
return json.ToString();
}
}
}

@ -0,0 +1,15 @@
namespace ZeroLevel.Qdrant.Models.Responces
{
public sealed class IndexOperation
{
public long operation_id { get; set; }
public string status { get; set; }
}
public sealed class CreateIndexResponse
{
public IndexOperation result { get; set; }
public string status { get; set; }
public float time { get; set; }
}
}

@ -0,0 +1,9 @@
namespace ZeroLevel.Qdrant.Models.Responces
{
public sealed class OperationResponse
{
public bool result { get; set; }
public string status { get; set; }
public float time { get; set; }
}
}

@ -0,0 +1,16 @@
namespace ZeroLevel.Qdrant.Models.Responces
{
public sealed class Point
{
public long id { get; set; }
public dynamic payload;
public double[] vector;
}
public sealed class PointResponse
{
public Point[] result { get; set; }
public string status { get; set; }
public float time { get; set; }
}
}

@ -0,0 +1,16 @@
namespace ZeroLevel.Qdrant.Models.Responces
{
public sealed class PointsOperationResult
{
public long operation_id { get; set; }
public string status { get; set; }
}
public sealed class PointsOperationResponse
{
public PointsOperationResult result { get; set; }
public string status { get; set; }
public float time { get; set; }
}
}

@ -0,0 +1,14 @@
namespace ZeroLevel.Qdrant.Models.Responces
{
public sealed class ScoredPoint
{
public long id { get; set; }
public double score { get; set; }
}
public sealed class SearchResponse
{
public ScoredPoint[] result { get; set; }
public string status { get; set; }
public float time { get; set; }
}
}

@ -0,0 +1,277 @@
using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using ZeroLevel.Models;
using ZeroLevel.Qdrant.Models.Filters;
using ZeroLevel.Qdrant.Models.Requests;
using ZeroLevel.Qdrant.Models.Responces;
namespace ZeroLevel.Qdrant
{
/*
https://qdrant.github.io/qdrant/redoc/index.html#operation/search_points
https://qdrant.tech/documentation/search/
*/
/// <summary>
/// Client for Qdrant API
/// </summary>
public class QdrantClient
{
private HttpClient CreateClient()
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => { return true; }
};
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
return new HttpClient(handler)
{
BaseAddress = _serverUri,
Timeout = TimeSpan.FromMinutes(5)
};
}
private readonly Uri _serverUri;
public QdrantClient(string host = "localhost", int port = 6333)
{
_serverUri = new Uri($"{host}:{port}");
}
#region API
#region Collection https://qdrant.tech/documentation/collections/
/// <summary>
/// Create collection
/// </summary>
/// <param name="name">Collection name</param>
/// <param name="distance">Cosine or Dot or Euclid</param>
/// <param name="vector_size">Count of elements in vectors</param>
/// <returns></returns>
public async Task<InvokeResult<OperationResponse>> CreateCollection(string name, string distance, int vector_size)
{
try
{
var collection = new CreateCollectionReqeust(name, distance, vector_size);
var json = JsonConvert.SerializeObject(collection);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections";
var response = await _request<OperationResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<OperationResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.CreateCollection] Name: {name}. Distance: {distance}. Vector size: {vector_size}");
return InvokeResult.Fault<OperationResponse>($"[QdrantClient.CreateCollection] Name: {name}\r\n{ex.ToString()}");
}
}
/// <summary>
/// Delete collection by name
/// </summary>
/// <param name="name">Collection name</param>
public async Task<InvokeResult<OperationResponse>> DeleteCollection(string name)
{
try
{
var collection = new DeleteCollectionRequest(name);
var json = JsonConvert.SerializeObject(collection);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections";
var response = await _request<OperationResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<OperationResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.DeleteCollection] Name: {name}.");
return InvokeResult.Fault<OperationResponse>($"[QdrantClient.DeleteCollection] Name: {name}\r\n{ex.ToString()}");
}
}
#endregion
#region Indexes https://qdrant.tech/documentation/indexing/
/// <summary>
/// For indexing, it is recommended to choose the field that limits the search result the most. As a rule, the more different values a payload value has, the more efficient the index will be used. You should not create an index for Boolean fields and fields with only a few possible values.
/// </summary>
public async Task<InvokeResult<CreateIndexResponse>> CreateIndex(string collection_name, string field_name)
{
try
{
var index = new CreateIndexRequest(field_name);
var json = JsonConvert.SerializeObject(index);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}";
var response = await _request<CreateIndexResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<CreateIndexResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.CreateIndex] Collection name: {collection_name}. Field name: {field_name}");
return InvokeResult.Fault<CreateIndexResponse>($"[QdrantClient.CreateIndex] Collection name: {collection_name}. Field name: {field_name}\r\n{ex.ToString()}");
}
}
#endregion
#region Search https://qdrant.tech/documentation/search/
/// <summary>
/// Searching for the nearest vectors
/// </summary>
public async Task<InvokeResult<SearchResponse>> Search(string collection_name, double[] vector, uint top, Filter filter = null)
{
try
{
var search = new SearchRequest { FloatVector = vector, Top = top, Filter = filter };
var json = search.ToJson();
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}/points/search";
var response = await _request<SearchResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<SearchResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.Search] Collection name: {collection_name}.");
return InvokeResult.Fault<SearchResponse>($"[QdrantClient.Search] Collection name: {collection_name}.\r\n{ex.ToString()}");
}
}
/// <summary>
/// Searching for the nearest vectors
/// </summary>
public async Task<InvokeResult<SearchResponse>> Search(string collection_name, long[] vector, uint top, Filter filter = null)
{
try
{
var search = new SearchRequest { IntegerVector = vector, Top = top, Filter = filter };
var json = search.ToJson();
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}/points/search";
var response = await _request<SearchResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<SearchResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.Search] Collection name: {collection_name}.");
return InvokeResult.Fault<SearchResponse>($"[QdrantClient.Search] Collection name: {collection_name}.\r\n{ex.ToString()}");
}
}
#endregion
#region Points https://qdrant.tech/documentation/points/
/// <summary>
/// There is a method for retrieving points by their ids.
/// </summary>
public async Task<InvokeResult<PointResponse>> Points(string collection_name, long[] ids)
{
try
{
var points = new PointsRequest { ids = ids };
var json = JsonConvert.SerializeObject(points);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}/points";
var response = await _request<PointResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<PointResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}.");
return InvokeResult.Fault<PointResponse>($"[QdrantClient.Points] Collection name: {collection_name}.\r\n{ex.ToString()}");
}
}
/// <summary>
/// Record-oriented of creating batches
/// </summary>
public async Task<InvokeResult<PointsOperationResponse>> PointsUpload<T>(string collection_name, UpsertPoint<T>[] points)
{
try
{
var points_request = new PointsUploadRequest<T> { upsert_points = new UpsertPoints<T> { points = points } };
var json = points_request.ToJSON();
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}";
var response = await _request<PointsOperationResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<PointsOperationResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}.");
return InvokeResult.Fault<PointsOperationResponse>($"[QdrantClient.Points] Collection name: {collection_name}.\r\n{ex.ToString()}");
}
}
/// <summary>
/// Column-oriented of creating batches
/// </summary>
public async Task<InvokeResult<PointsOperationResponse>> PointsColumnUpload<T>(string collection_name, ColumnPoints<T> points)
{
try
{
var points_request = new PointsColumnUploadRequest<T> { upsert_points = new UpsertColumnPoints<T> { batch = points } };
var json = points_request.ToJSON();
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}";
var response = await _request<PointsOperationResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<PointsOperationResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}.");
return InvokeResult.Fault<PointsOperationResponse>($"[QdrantClient.Points] Collection name: {collection_name}.\r\n{ex.ToString()}");
}
}
/// <summary>
/// Delete points by their ids.
/// </summary>
public async Task<InvokeResult<PointsOperationResponse>> DeletePoints(string collection_name, long[] ids)
{
try
{
var points = new DeletePointsRequest { delete_points = new DeletePoints { ids = ids } };
var json = JsonConvert.SerializeObject(points);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"/collections/{collection_name}";
var response = await _request<PointsOperationResponse>(url, new HttpMethod("POST"), data);
return InvokeResult.Succeeding<PointsOperationResponse>(response);
}
catch (Exception ex)
{
Log.Error(ex, $"[QdrantClient.DeleteCollection] Name: {collection_name}.");
return InvokeResult.Fault<PointsOperationResponse>($"[QdrantClient.DeleteCollection] Name: {collection_name}\r\n{ex.ToString()}");
}
}
#endregion
#endregion
#region Private
private async Task<T> _request<T>(string url, HttpMethod method, HttpContent content = null)
{
var json = await _request(url, method, content);
return JsonConvert.DeserializeObject<T>(json);
}
private async Task<string> _request(string url, HttpMethod method, HttpContent content = null)
{
var fullUrl = new Uri(_serverUri, url);
var message = new HttpRequestMessage(method, fullUrl) { Content = content };
using (var client = CreateClient())
{
var response = await client.SendAsync(message);
var result = await response.Content.ReadAsStringAsync();
var jsonPrint = result?.Length >= 5000 ? "<BIG CONTENT>" : result;
if (response.IsSuccessStatusCode == false)
{
throw new Exception($"Not SuccessStatusCode {method} {fullUrl}. Status: {response.StatusCode} {response.ReasonPhrase}. Content: {jsonPrint}");
}
return result;
}
}
#endregion
}
}

@ -0,0 +1,168 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ZeroLevel.Qdrant.DataAttributes;
using ZeroLevel.Qdrant.Models;
using ZeroLevel.Services.ObjectMapping;
using ZeroLevel.Services.Reflection;
using ZeroLevel.Services.Serialization;
namespace ZeroLevel.Qdrant.Services
{
public class QdrantJsonConverter<T>
{
private static string KeywordToString(IMemberInfo member, object v)
{
var text = TypeHelpers.IsString(member.ClrType) ? v as string : v.ToString();
if (string.IsNullOrEmpty(text))
{
return "null";
}
else
{
return $"\"{JsonEscaper.EscapeString(text)}\"";
}
}
/*
integer - 64-bit integer in the range -9223372036854775808 to 9223372036854775807. array of long
float - 64-bit floating point number. array of double
keyword - string value. array of strings
geo - Geographical coordinates. Example: { "lon": 52.5200, "lat": 13.4050 } array of lon&lat of double
*/
private const string KYEWORD_TYPE = "keyword";
private const string GEO_TYPE = "geo";
private const string FLOAT_TYPE = "float";
private const string INTEGER_TYPE = "integer";
public string ToJson(T value)
{
var json = new StringBuilder();
var map = TypeMapper.Create<T>();
foreach (var member in map.Members)
{
var val = member.Getter(value);
var type = KYEWORD_TYPE;
var attributes = member.Original.GetCustomAttributes(typeof(QdrantAttribute), true);
if (attributes != null && attributes.Any())
{
var dataAttribute = attributes[0];
if (dataAttribute is KeywordAttribute)
{
type = KYEWORD_TYPE;
}
else if (dataAttribute is FloatAttribute)
{
type = FLOAT_TYPE;
}
else if (dataAttribute is IntegerAttribute)
{
type = INTEGER_TYPE;
}
else if (dataAttribute is GeoAttribute)
{
type = GEO_TYPE;
}
}
else
{
var item_type = member.ClrType;
// autodetect type
if (TypeHelpers.IsArray(item_type))
{
item_type = item_type.GetElementType();
}
else if (TypeHelpers.IsEnumerable(item_type))
{
item_type = TypeHelpers.GetElementTypeOfEnumerable(item_type);
}
if (item_type == typeof(float) || item_type == typeof(double) || item_type == typeof(decimal))
{
type = FLOAT_TYPE;
}
else if (item_type == typeof(int) || item_type == typeof(long) || item_type == typeof(byte) ||
item_type == typeof(short) || item_type == typeof(uint) || item_type == typeof(ulong) ||
item_type == typeof(ushort))
{
type = INTEGER_TYPE;
}
else if (item_type == typeof(Location))
{
type = GEO_TYPE;
}
}
switch (type)
{
case KYEWORD_TYPE:
if (TypeHelpers.IsEnumerable(member.ClrType) && TypeHelpers.IsString(member.ClrType) == false)
{
var arr = val as IEnumerable;
json.Append($"\"{member.Name}\": {{ \"type\": \"keyword\", \"value\": [ {string.Join(", ", E(arr).Select(v => KeywordToString(member, v)))}] }},");
}
else
{
json.Append($"\"{member.Name}\": {{ \"type\": \"keyword\", \"value\":{KeywordToString(member, val)} }},");
}
break;
case GEO_TYPE:
if (TypeHelpers.IsEnumerable(member.ClrType) && TypeHelpers.IsString(member.ClrType) == false)
{
var arr = val as IEnumerable;
json.Append($"\"{member.Name}\": {{ \"type\": \"geo\", \"value\": [ {string.Join(",", E(arr).Select(v => v as Location).Where(l => l != null).Select(l => $" {{ \"lon\":{l.lon.ConvertToString()}, \"lat\":{l.lat.ConvertToString()} }}"))}] }},");
}
else
{
Location l = val as Location;
if (l != null)
{
json.Append($"\"{member.Name}\": {{ \"type\": \"geo\", \"value\": {{ \"lon\":{l.lon.ConvertToString()}, \"lat\":{l.lat.ConvertToString()} }} }},");
}
}
break;
case FLOAT_TYPE:
if (TypeHelpers.IsEnumerable(member.ClrType) && TypeHelpers.IsString(member.ClrType) == false)
{
var arr = val as IEnumerable;
json.Append($"\"{member.Name}\": {{ \"type\": \"float\", \"value\": [ {string.Join(",", E(arr).Select(v => Convert.ToDouble(v).ConvertToString()))}] }},");
}
else
{
json.Append($"\"{member.Name}\": {{ \"type\": \"float\", \"value\": {Convert.ToDouble(val).ConvertToString()} }},");
}
break;
case INTEGER_TYPE:
if (TypeHelpers.IsEnumerable(member.ClrType) && TypeHelpers.IsString(member.ClrType) == false)
{
var arr = val as IEnumerable;
json.Append($"\"{member.Name}\": {{ \"type\": \"integer\", \"value\": [ {string.Join(",", E(arr).Select(v => Convert.ToInt64(v)))}] }},");
}
else
{
json.Append($"\"{member.Name}\": {{ \"type\": \"integer\", \"value\": {Convert.ToInt64(val)} }},");
}
break;
}
}
if (json[json.Length - 1] == ',')
{
json.Remove(json.Length - 1, 1);
}
return json.ToString();
}
public IEnumerable<object> E(IEnumerable e)
{
if (e != null)
{
foreach (var i in e)
{
yield return i;
}
}
}
}
}

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Platforms>AnyCPU;x64;x86</Platforms>
<Version>1.0.0.0</Version>
<Authors>ogoun</Authors>
<Copyright>Copyright Ogoun 2021</Copyright>
<PackageProjectUrl>https://github.com/ogoun/Zero</PackageProjectUrl>
<PackageIcon>zero.png</PackageIcon>
<PackageIconUrl />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZeroLevel\ZeroLevel.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="..\zero.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project>

@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "ConnectionTest\Cl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "ConnectionTest\Server\Server.csproj", "{3496A688-0749-48C2-BD60-ABB42A5C17C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.Qdrant", "ZeroLevel.Qdrant\ZeroLevel.Qdrant.csproj", "{7188B89E-96EB-4EFB-AAFB-D0A823031F99}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -271,6 +273,18 @@ Global
{3496A688-0749-48C2-BD60-ABB42A5C17C9}.Release|x64.Build.0 = Release|x64
{3496A688-0749-48C2-BD60-ABB42A5C17C9}.Release|x86.ActiveCfg = Release|x86
{3496A688-0749-48C2-BD60-ABB42A5C17C9}.Release|x86.Build.0 = Release|x86
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Debug|x64.ActiveCfg = Debug|x64
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Debug|x64.Build.0 = Debug|x64
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Debug|x86.ActiveCfg = Debug|x86
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Debug|x86.Build.0 = Debug|x86
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Release|Any CPU.Build.0 = Release|Any CPU
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Release|x64.ActiveCfg = Release|x64
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Release|x64.Build.0 = Release|x64
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Release|x86.ActiveCfg = Release|x86
{7188B89E-96EB-4EFB-AAFB-D0A823031F99}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

Loading…
Cancel
Save

Powered by TurnKey Linux.