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
}
}