diff --git a/ZeroLevel.Qdrant/DataAttributes/FloatAttribute.cs b/ZeroLevel.Qdrant/DataAttributes/FloatAttribute.cs
new file mode 100644
index 0000000..c9c0534
--- /dev/null
+++ b/ZeroLevel.Qdrant/DataAttributes/FloatAttribute.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Qdrant.DataAttributes
+{
+ ///
+ /// Attribute for directly specifying the 'float' data type of qdrant
+ ///
+ public class FloatAttribute
+ : QdrantAttribute
+ {
+ }
+}
diff --git a/ZeroLevel.Qdrant/DataAttributes/GeoAttribute.cs b/ZeroLevel.Qdrant/DataAttributes/GeoAttribute.cs
new file mode 100644
index 0000000..85fa4f4
--- /dev/null
+++ b/ZeroLevel.Qdrant/DataAttributes/GeoAttribute.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Qdrant.DataAttributes
+{
+ ///
+ /// Attribute for directly specifying the 'geo' data type of qdrant
+ ///
+ public class GeoAttribute
+ : QdrantAttribute
+ {
+ }
+}
diff --git a/ZeroLevel.Qdrant/DataAttributes/IntegerAttribute.cs b/ZeroLevel.Qdrant/DataAttributes/IntegerAttribute.cs
new file mode 100644
index 0000000..5087161
--- /dev/null
+++ b/ZeroLevel.Qdrant/DataAttributes/IntegerAttribute.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Qdrant.DataAttributes
+{
+ ///
+ /// Attribute for directly specifying the 'integer' data type of qdrant
+ ///
+ public class IntegerAttribute
+ : QdrantAttribute
+ {
+ }
+}
diff --git a/ZeroLevel.Qdrant/DataAttributes/KeywordAttribute.cs b/ZeroLevel.Qdrant/DataAttributes/KeywordAttribute.cs
new file mode 100644
index 0000000..c65f10d
--- /dev/null
+++ b/ZeroLevel.Qdrant/DataAttributes/KeywordAttribute.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Qdrant.DataAttributes
+{
+ ///
+ /// Attribute for directly specifying the 'keyword' data type of qdrant
+ ///
+ public class KeywordAttribute
+ : QdrantAttribute
+ {
+ }
+}
diff --git a/ZeroLevel.Qdrant/DataAttributes/QdrantAttribute.cs b/ZeroLevel.Qdrant/DataAttributes/QdrantAttribute.cs
new file mode 100644
index 0000000..a464f15
--- /dev/null
+++ b/ZeroLevel.Qdrant/DataAttributes/QdrantAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ZeroLevel.Qdrant.DataAttributes
+{
+ ///
+ /// Attribute for directly specifying the data type of qdrant
+ ///
+ public abstract class QdrantAttribute
+ : Attribute
+ {
+ }
+}
diff --git a/ZeroLevel.Qdrant/DoubleExtensions.cs b/ZeroLevel.Qdrant/DoubleExtensions.cs
new file mode 100644
index 0000000..1062815
--- /dev/null
+++ b/ZeroLevel.Qdrant/DoubleExtensions.cs
@@ -0,0 +1,10 @@
+namespace ZeroLevel.Qdrant
+{
+ public static class DoubleExtensions
+ {
+ public static string ConvertToString(this double num)
+ {
+ return num.ToString().Replace(',', '.');
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Filters/Condition.cs b/ZeroLevel.Qdrant/Models/Filters/Condition.cs
new file mode 100644
index 0000000..4a6d865
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Filters/Condition.cs
@@ -0,0 +1,81 @@
+namespace ZeroLevel.Qdrant.Models.Filters
+{
+ ///
+ /// Condition for qdrant filters
+ ///
+ 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;
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Filters/Filter.cs b/ZeroLevel.Qdrant/Models/Filters/Filter.cs
new file mode 100644
index 0000000..b09a1e5
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Filters/Filter.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ZeroLevel.Qdrant.Models.Filters
+{
+ ///
+ /// Filter for search in qdrant
+ ///
+ public class Filter
+ {
+ private List _groups = new List();
+
+ 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();
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Filters/Group.cs b/ZeroLevel.Qdrant/Models/Filters/Group.cs
new file mode 100644
index 0000000..a03a9be
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Filters/Group.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ZeroLevel.Qdrant.Models.Filters
+{
+ public class Group
+ : Operand
+ {
+ private List _items = new List();
+ 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()))}]";
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Filters/GroupOperator.cs b/ZeroLevel.Qdrant/Models/Filters/GroupOperator.cs
new file mode 100644
index 0000000..29008cd
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Filters/GroupOperator.cs
@@ -0,0 +1,9 @@
+namespace ZeroLevel.Qdrant.Models.Filters
+{
+ public enum GroupOperator
+ {
+ Must,
+ Should,
+ MustNot
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Filters/Operand.cs b/ZeroLevel.Qdrant/Models/Filters/Operand.cs
new file mode 100644
index 0000000..b468880
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Filters/Operand.cs
@@ -0,0 +1,7 @@
+namespace ZeroLevel.Qdrant.Models.Filters
+{
+ public abstract class Operand
+ {
+ public abstract string ToJSON();
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Location.cs b/ZeroLevel.Qdrant/Models/Location.cs
new file mode 100644
index 0000000..3b28a7d
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Location.cs
@@ -0,0 +1,8 @@
+namespace ZeroLevel.Qdrant.Models
+{
+ public class Location
+ {
+ public double lon { get; set; }
+ public double lat { get; set; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/CreateCollectionReqeust.cs b/ZeroLevel.Qdrant/Models/Requests/CreateCollectionReqeust.cs
new file mode 100644
index 0000000..9214647
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/CreateCollectionReqeust.cs
@@ -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
+ };
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/CreateIndexRequest.cs b/ZeroLevel.Qdrant/Models/Requests/CreateIndexRequest.cs
new file mode 100644
index 0000000..663dc63
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/CreateIndexRequest.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/DeleteCollectionRequest.cs b/ZeroLevel.Qdrant/Models/Requests/DeleteCollectionRequest.cs
new file mode 100644
index 0000000..4cfe690
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/DeleteCollectionRequest.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/DeletePointsRequest.cs b/ZeroLevel.Qdrant/Models/Requests/DeletePointsRequest.cs
new file mode 100644
index 0000000..cfb899f
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/DeletePointsRequest.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/PointsRequest.cs b/ZeroLevel.Qdrant/Models/Requests/PointsRequest.cs
new file mode 100644
index 0000000..9e5cd00
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/PointsRequest.cs
@@ -0,0 +1,7 @@
+namespace ZeroLevel.Qdrant.Models.Requests
+{
+ internal sealed class PointsRequest
+ {
+ public long[] ids { get; set; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/PointsUploadRequest.cs b/ZeroLevel.Qdrant/Models/Requests/PointsUploadRequest.cs
new file mode 100644
index 0000000..b4b55a1
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/PointsUploadRequest.cs
@@ -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
+ {
+ public long? id { get; set; } = null;
+ public T payload { get; set; }
+ public double[] vector { get; set; }
+ }
+ public sealed class UpsertPoints
+ {
+ public UpsertPoint[] points { get; set; }
+ }
+ public sealed class PointsUploadRequest
+ {
+ private static IEverythingStorage _cachee = EverythingStorage.Create();
+ public UpsertPoints upsert_points { get; set; }
+
+ public string ToJSON()
+ {
+ if (!_cachee.ContainsKey>("converter"))
+ {
+ _cachee.Add("converter", new QdrantJsonConverter());
+ }
+ var converter = _cachee.Get>("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
+ {
+ public long[] ids { get; set; }
+ public T[] payloads { get; set; }
+ public double[,] vectors { get; set; }
+ }
+
+ public sealed class UpsertColumnPoints
+ {
+ public ColumnPoints batch { get; set; }
+ }
+
+ public sealed class PointsColumnUploadRequest
+ {
+ private static IEverythingStorage _cachee = EverythingStorage.Create();
+ public UpsertColumnPoints upsert_points { get; set; }
+
+ public string ToJSON()
+ {
+ if (!_cachee.ContainsKey>("converter"))
+ {
+ _cachee.Add("converter", new QdrantJsonConverter());
+ }
+ var converter = _cachee.Get>("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();
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Requests/SearchRequest.cs b/ZeroLevel.Qdrant/Models/Requests/SearchRequest.cs
new file mode 100644
index 0000000..0df42de
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Requests/SearchRequest.cs
@@ -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
+ {
+ ///
+ /// Look only for points which satisfies this conditions
+ ///
+ public Filter Filter { get; set; }
+ ///
+ /// Look for vectors closest to this
+ ///
+ public double[] FloatVector { get; set; }
+ public long[] IntegerVector { get; set; }
+ ///
+ /// Max number of result to return
+ ///
+ public uint Top { get; set; }
+ ///
+ /// 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.
+ ///
+ 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();
+ }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Responces/CreateIndexResponse.cs b/ZeroLevel.Qdrant/Models/Responces/CreateIndexResponse.cs
new file mode 100644
index 0000000..87df28b
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Responces/CreateIndexResponse.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Responces/OperationResponse.cs b/ZeroLevel.Qdrant/Models/Responces/OperationResponse.cs
new file mode 100644
index 0000000..db0520e
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Responces/OperationResponse.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Responces/PointResponse.cs b/ZeroLevel.Qdrant/Models/Responces/PointResponse.cs
new file mode 100644
index 0000000..b932fb1
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Responces/PointResponse.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Responces/PointsOperationResponse.cs b/ZeroLevel.Qdrant/Models/Responces/PointsOperationResponse.cs
new file mode 100644
index 0000000..b43df81
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Responces/PointsOperationResponse.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/Models/Responces/SearchResponse.cs b/ZeroLevel.Qdrant/Models/Responces/SearchResponse.cs
new file mode 100644
index 0000000..efcb437
--- /dev/null
+++ b/ZeroLevel.Qdrant/Models/Responces/SearchResponse.cs
@@ -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; }
+ }
+}
diff --git a/ZeroLevel.Qdrant/QdrantClient.cs b/ZeroLevel.Qdrant/QdrantClient.cs
new file mode 100644
index 0000000..0e69c40
--- /dev/null
+++ b/ZeroLevel.Qdrant/QdrantClient.cs
@@ -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/
+ */
+ ///
+ /// Client for Qdrant API
+ ///
+ 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/
+ ///
+ /// Create collection
+ ///
+ /// Collection name
+ /// Cosine or Dot or Euclid
+ /// Count of elements in vectors
+ ///
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.CreateCollection] Name: {name}. Distance: {distance}. Vector size: {vector_size}");
+ return InvokeResult.Fault($"[QdrantClient.CreateCollection] Name: {name}\r\n{ex.ToString()}");
+ }
+ }
+ ///
+ /// Delete collection by name
+ ///
+ /// Collection name
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.DeleteCollection] Name: {name}.");
+ return InvokeResult.Fault($"[QdrantClient.DeleteCollection] Name: {name}\r\n{ex.ToString()}");
+ }
+ }
+ #endregion
+
+ #region Indexes https://qdrant.tech/documentation/indexing/
+ ///
+ /// 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.
+ ///
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.CreateIndex] Collection name: {collection_name}. Field name: {field_name}");
+ return InvokeResult.Fault($"[QdrantClient.CreateIndex] Collection name: {collection_name}. Field name: {field_name}\r\n{ex.ToString()}");
+ }
+ }
+ #endregion
+
+ #region Search https://qdrant.tech/documentation/search/
+ ///
+ /// Searching for the nearest vectors
+ ///
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.Search] Collection name: {collection_name}.");
+ return InvokeResult.Fault($"[QdrantClient.Search] Collection name: {collection_name}.\r\n{ex.ToString()}");
+ }
+ }
+ ///
+ /// Searching for the nearest vectors
+ ///
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.Search] Collection name: {collection_name}.");
+ return InvokeResult.Fault($"[QdrantClient.Search] Collection name: {collection_name}.\r\n{ex.ToString()}");
+ }
+ }
+ #endregion
+
+ #region Points https://qdrant.tech/documentation/points/
+ ///
+ /// There is a method for retrieving points by their ids.
+ ///
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}.");
+ return InvokeResult.Fault($"[QdrantClient.Points] Collection name: {collection_name}.\r\n{ex.ToString()}");
+ }
+ }
+ ///
+ /// Record-oriented of creating batches
+ ///
+ public async Task> PointsUpload(string collection_name, UpsertPoint[] points)
+ {
+ try
+ {
+ var points_request = new PointsUploadRequest { upsert_points = new UpsertPoints { 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}.");
+ return InvokeResult.Fault($"[QdrantClient.Points] Collection name: {collection_name}.\r\n{ex.ToString()}");
+ }
+ }
+ ///
+ /// Column-oriented of creating batches
+ ///
+ public async Task> PointsColumnUpload(string collection_name, ColumnPoints points)
+ {
+ try
+ {
+ var points_request = new PointsColumnUploadRequest { upsert_points = new UpsertColumnPoints { 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}.");
+ return InvokeResult.Fault($"[QdrantClient.Points] Collection name: {collection_name}.\r\n{ex.ToString()}");
+ }
+ }
+ ///
+ /// Delete points by their ids.
+ ///
+ public async Task> 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(url, new HttpMethod("POST"), data);
+ return InvokeResult.Succeeding(response);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"[QdrantClient.DeleteCollection] Name: {collection_name}.");
+ return InvokeResult.Fault($"[QdrantClient.DeleteCollection] Name: {collection_name}\r\n{ex.ToString()}");
+ }
+ }
+ #endregion
+
+ #endregion
+
+ #region Private
+ private async Task _request(string url, HttpMethod method, HttpContent content = null)
+ {
+ var json = await _request(url, method, content);
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ private async Task _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 ? "" : result;
+ if (response.IsSuccessStatusCode == false)
+ {
+ throw new Exception($"Not SuccessStatusCode {method} {fullUrl}. Status: {response.StatusCode} {response.ReasonPhrase}. Content: {jsonPrint}");
+ }
+ return result;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/ZeroLevel.Qdrant/Services/QdrantJsonConverter.cs b/ZeroLevel.Qdrant/Services/QdrantJsonConverter.cs
new file mode 100644
index 0000000..f17035e
--- /dev/null
+++ b/ZeroLevel.Qdrant/Services/QdrantJsonConverter.cs
@@ -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
+ {
+ 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();
+ 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