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 const int DEFAULT_OPERATION_TIMEOUT_S = 30; 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.github.io/qdrant/redoc/index.html#tag/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, bool? on_disk_payload) { try { var collection = new CreateCollectionReqeust(distance, vector_size, on_disk_payload); var json = JsonConvert.SerializeObject(collection); var data = new StringContent(json, Encoding.UTF8, "application/json"); var url = $"/collections/{name}"; var response = await _request(url, new HttpMethod("PUT"), 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, int timeout = DEFAULT_OPERATION_TIMEOUT_S) { try { var url = $"/collections/{name}?timeout={timeout}"; var response = await _request(url, new HttpMethod("DELETE"), null); 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, IndexFieldType field_type) { try { var index = new CreateIndexRequest(field_name, field_type); var json = JsonConvert.SerializeObject(index); var data = new StringContent(json, Encoding.UTF8, "application/json"); var url = $"/collections/{collection_name}/index"; var response = await _request(url, new HttpMethod("PUT"), 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> GetPoint(string collection_name, long id) { try { string url = $"/collections/{collection_name}/points/{id}"; var response = await _request(url, new HttpMethod("GET"), null); return InvokeResult.Succeeding(response); } catch (Exception ex) { Log.Error(ex, $"[QdrantClient.Points] Collection name: {collection_name}."); return InvokeResult.Fault($"[QdrantClient.GetPoint] Collection name: {collection_name}. Point ID: {id}\r\n{ex.ToString()}"); } } /// /// There is a method for retrieving points by their ids. /// public async Task> GetPoints(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"); string 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.GetPoints] Collection name: {collection_name}.\r\n{ex.ToString()}"); } } /// /// There is a method for retrieving points by their ids. /// public async Task> Scroll(string collection_name, Filter filter, long limit, long offset = 0, bool with_vector = true, bool with_payload = true) { try { var scroll = new ScrollRequest { Filter = filter, Limit = limit, Offset = offset, WithPayload = with_payload, WithVector = with_vector }; var json = scroll.ToJson(); var data = new StringContent(json, Encoding.UTF8, "application/json"); string url = url = $"/collections/{collection_name}/points/scroll"; var response = await _request(url, new HttpMethod("POST"), data); return InvokeResult.Succeeding(response); } catch (Exception ex) { Log.Error(ex, $"[QdrantClient.Scroll] Collection name: {collection_name}."); return InvokeResult.Fault($"[QdrantClient.Scroll] Collection name: {collection_name}.\r\n{ex.ToString()}"); } } /// /// Record-oriented of creating batches /// public async Task> UpsertPoints(string collection_name, PointsUpsertRequest points) { try { var json = points.ToJSON(); var data = new StringContent(json, Encoding.UTF8, "application/json"); var url = $"/collections/{collection_name}/points"; var response = await _request(url, new HttpMethod("PUT"), data); return InvokeResult.Succeeding(response); } catch (Exception ex) { Log.Error(ex, $"[QdrantClient.UpsertPoints] Collection name: {collection_name}."); return InvokeResult.Fault($"[QdrantClient.UpsertPoints] 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 } }