diff --git a/README.md b/README.md index b0f87fe..2074948 100644 --- a/README.md +++ b/README.md @@ -363,9 +363,8 @@ import io.endee.client.types.IndexDescription; IndexDescription info = index.describe(); System.out.println(info); -// IndexDescription{name='my_index', spaceType=COSINE, dimension=384, -// sparseDimension=0, isHybrid=false, count=1000, -// precision=INT16, m=16} +// {name='my_index', spaceType= COSINE, dimension=384, precision=INT16, +// count=1000, isHybrid=false, sparseDimension=0, M=16, efCon=128} ``` ### Check if Index is Hybrid @@ -608,6 +607,23 @@ VectorItem.builder(String id, double[] vector) | `count` | `long` | Number of vectors | | `precision` | `Precision` | Quantization precision | | `m` | `int` | Graph connectivity | +| `efCon` | `int` | Construction-time quality parameter | + +## Code Formatting + +This project uses [Spotless](https://github.com/diffplug/spotless) with [Google Java Format](https://github.com/google/google-java-format) to enforce consistent code style. + +**Format all source files:** +```bash +mvn spotless:apply +``` + +**Check formatting without modifying files:** +```bash +mvn spotless:check +``` + +Formatting is also checked automatically as part of `mvn verify` — CI will fail if any file is not properly formatted. ## Dependencies diff --git a/pom.xml b/pom.xml index b40a1e7..1c22ac4 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,31 @@ + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + 1.22.0 + + + + + + + + + + + check + + verify + + + + org.apache.maven.plugins maven-javadoc-plugin diff --git a/src/main/java/io/endee/client/Endee.java b/src/main/java/io/endee/client/Endee.java index 49632be..b0b6ea0 100644 --- a/src/main/java/io/endee/client/Endee.java +++ b/src/main/java/io/endee/client/Endee.java @@ -10,9 +10,6 @@ import io.endee.client.types.SpaceType; import io.endee.client.util.JsonUtils; import io.endee.client.util.ValidationUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -20,14 +17,14 @@ import java.net.http.HttpResponse; import java.time.Duration; import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Main Endee client for Endee-DB. * - *

- * Example usage: - *

- * + *

Example usage: + * *

{@code
  * Endee client = new Endee("auth-token");
  *
@@ -43,246 +40,252 @@
  * }
*/ public class Endee { - private static final Logger logger = LoggerFactory.getLogger(Endee.class); - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); - private static final int MAX_DIMENSION = 10000; - - private String token; - private String baseUrl; - private final int version; - private final HttpClient httpClient; - private final ObjectMapper objectMapper; - - /** - * Creates a new Endee client without authentication. - * Uses local server at http://127.0.0.1:8080/api/v1 - */ - public Endee() { - this(null); - this.baseUrl = "http://127.0.0.1:8080/api/v1"; + private static final Logger logger = LoggerFactory.getLogger(Endee.class); + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); + private static final int MAX_DIMENSION = 10000; + + private String token; + private String baseUrl; + private final int version; + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + + /** + * Creates a new Endee client without authentication. Uses local server at + * http://127.0.0.1:8080/api/v1 + */ + public Endee() { + this(null); + this.baseUrl = "http://127.0.0.1:8080/api/v1"; + } + + /** + * Creates a new Endee client. + * + * @param token the Auth token (optional) + */ + public Endee(String token) { + this.token = token; + this.baseUrl = "http://127.0.0.1:8080/api/v1"; + this.version = 1; + this.objectMapper = new ObjectMapper(); + + if (token != null && !token.isEmpty()) { + String[] tokenParts = token.split(":"); + if (tokenParts.length > 2) { + this.baseUrl = "https://" + tokenParts[2] + ".endee.io/api/v1"; + this.token = tokenParts[0] + ":" + tokenParts[1]; + } } - /** - * Creates a new Endee client. - * - * @param token the Auth token (optional) - */ - public Endee(String token) { - this.token = token; - this.baseUrl = "http://127.0.0.1:8080/api/v1"; - this.version = 1; - this.objectMapper = new ObjectMapper(); - - if (token != null && !token.isEmpty()) { - String[] tokenParts = token.split(":"); - if (tokenParts.length > 2) { - this.baseUrl = "https://" + tokenParts[2] + ".endee.io/api/v1"; - this.token = tokenParts[0] + ":" + tokenParts[1]; - } - } - - this.httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .connectTimeout(DEFAULT_TIMEOUT) - .build(); + this.httpClient = + HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(DEFAULT_TIMEOUT) + .build(); + } + + /** + * Sets a custom base URL for the API. + * + * @param url the base URL + * @return the URL that was set + */ + public String setBaseUrl(String url) { + this.baseUrl = url; + return url; + } + + /** + * Creates a new index. + * + * @param options the index creation options + * @return success message + * @throws EndeeException if the operation fails + */ + public String createIndex(CreateIndexOptions options) { + if (!ValidationUtils.isValidIndexName(options.getName())) { + throw new IllegalArgumentException( + "Invalid index name. Index name must be alphanumeric and can contain underscores and less than 48 characters"); } - - /** - * Sets a custom base URL for the API. - * - * @param url the base URL - * @return the URL that was set - */ - public String setBaseUrl(String url) { - this.baseUrl = url; - return url; + if (options.getDimension() > MAX_DIMENSION) { + throw new IllegalArgumentException("Dimension cannot be greater than " + MAX_DIMENSION); } - - /** - * Creates a new index. - * - * @param options the index creation options - * @return success message - * @throws EndeeException if the operation fails - */ - public String createIndex(CreateIndexOptions options) { - if (!ValidationUtils.isValidIndexName(options.getName())) { - throw new IllegalArgumentException( - "Invalid index name. Index name must be alphanumeric and can contain underscores and less than 48 characters"); - } - if (options.getDimension() > MAX_DIMENSION) { - throw new IllegalArgumentException("Dimension cannot be greater than " + MAX_DIMENSION); - } - if (options.getSparseDimension() != null && options.getSparseDimension() < 0) { - throw new IllegalArgumentException("Sparse dimension cannot be less than 0"); - } - - String normalizedSpaceType = options.getSpaceType().getValue().toLowerCase(); - if (!List.of("cosine", "l2", "ip").contains(normalizedSpaceType)) { - throw new IllegalArgumentException("Invalid space type: " + options.getSpaceType()); - } - - Map data = new HashMap<>(); - data.put("index_name", options.getName()); - data.put("dim", options.getDimension()); - data.put("space_type", normalizedSpaceType); - data.put("M", options.getM()); - data.put("ef_con", options.getEfCon()); - data.put("checksum", -1); - data.put("precision", options.getPrecision().getValue()); - - if (options.getSparseDimension() != null) { - data.put("sparse_dim", options.getSparseDimension()); - } - if (options.getVersion() != null) { - data.put("version", options.getVersion()); - } - - try { - HttpRequest request = buildPostRequest("/index/create", data); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - logger.error("Error: {}", response.body()); - EndeeApiException.raiseException(response.statusCode(), response.body()); - } - - return "Index created successfully"; - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to create index", e); - } + if (options.getSparseDimension() != null && options.getSparseDimension() < 0) { + throw new IllegalArgumentException("Sparse dimension cannot be less than 0"); } - /** - * Lists all indexes. - * - * @return list of index information - * @throws EndeeException if the operation fails - */ - public String listIndexes() { - try { - HttpRequest request = buildGetRequest("/index/list"); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - return response.body(); - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to list indexes", e); - } + String normalizedSpaceType = options.getSpaceType().getValue().toLowerCase(); + if (!List.of("cosine", "l2", "ip").contains(normalizedSpaceType)) { + throw new IllegalArgumentException("Invalid space type: " + options.getSpaceType()); } - /** - * Deletes an index. - * - * @param name the index name to delete - * @return success message - * @throws EndeeException if the operation fails - */ - public String deleteIndex(String name) { - try { - HttpRequest request = buildDeleteRequest("/index/" + name + "/delete"); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - logger.error("Error: {}", response.body()); - EndeeApiException.raiseException(response.statusCode(), response.body()); - } - - return "Index " + name + " deleted successfully"; - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to delete index", e); - } + Map data = new HashMap<>(); + data.put("index_name", options.getName()); + data.put("dim", options.getDimension()); + data.put("space_type", normalizedSpaceType); + data.put("M", options.getM()); + data.put("ef_con", options.getEfCon()); + data.put("checksum", -1); + data.put("precision", options.getPrecision().getValue()); + + if (options.getSparseDimension() != null) { + data.put("sparse_dim", options.getSparseDimension()); } - - /** - * Gets an index by name. - * - * @param name the index name - * @return the Index object for performing vector operations - * @throws EndeeException if the operation fails - */ - public Index getIndex(String name) { - try { - HttpRequest request = buildGetRequest("/index/" + name + "/info"); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - EndeeApiException.raiseException(response.statusCode(), response.body()); - } - - JsonNode data = objectMapper.readTree(response.body()); - - IndexInfo indexInfo = new IndexInfo(); - indexInfo.setSpaceType(SpaceType.fromValue(data.get("space_type").asText())); - indexInfo.setDimension(data.get("dimension").asInt()); - indexInfo.setTotalElements(data.get("total_elements").asLong()); - indexInfo.setPrecision(Precision.fromValue(data.get("precision").asText())); - indexInfo.setM(data.get("M").asInt()); - indexInfo.setChecksum(data.get("checksum").asLong()); - - if (data.has("version") && !data.get("version").isNull()) { - indexInfo.setVersion(data.get("version").asInt()); - } - if (data.has("sparse_dim") && !data.get("sparse_dim").isNull()) { - indexInfo.setSparseDimension(data.get("sparse_dim").asInt()); - } - - return new Index(name, token, baseUrl, version, indexInfo); - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to get index", e); - } + if (options.getVersion() != null) { + data.put("version", options.getVersion()); } - private HttpRequest buildGetRequest(String path) { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + path)) - .header("Content-Type", "application/json") - .timeout(DEFAULT_TIMEOUT) - .GET(); - - if (token != null && !token.isEmpty()) { - builder.header("Authorization", token); - } - - return builder.build(); + try { + HttpRequest request = buildPostRequest("/index/create", data); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + logger.error("Error: {}", response.body()); + EndeeApiException.raiseException(response.statusCode(), response.body()); + } + + return "Index created successfully"; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to create index", e); + } + } + + /** + * Lists all indexes. + * + * @return list of index information + * @throws EndeeException if the operation fails + */ + public String listIndexes() { + try { + HttpRequest request = buildGetRequest("/index/list"); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return response.body(); + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to list indexes", e); + } + } + + /** + * Deletes an index. + * + * @param name the index name to delete + * @return success message + * @throws EndeeException if the operation fails + */ + public String deleteIndex(String name) { + try { + HttpRequest request = buildDeleteRequest("/index/" + name + "/delete"); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + logger.error("Error: {}", response.body()); + EndeeApiException.raiseException(response.statusCode(), response.body()); + } + + return "Index " + name + " deleted successfully"; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to delete index", e); + } + } + + /** + * Gets an index by name. + * + * @param name the index name + * @return the Index object for performing vector operations + * @throws EndeeException if the operation fails + */ + public Index getIndex(String name) { + try { + HttpRequest request = buildGetRequest("/index/" + name + "/info"); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), response.body()); + } + + JsonNode data = objectMapper.readTree(response.body()); + + IndexInfo indexInfo = new IndexInfo(); + indexInfo.setSpaceType(SpaceType.fromValue(data.get("space_type").asText())); + indexInfo.setDimension(data.get("dimension").asInt()); + indexInfo.setTotalElements(data.get("total_elements").asLong()); + indexInfo.setPrecision(Precision.fromValue(data.get("precision").asText())); + indexInfo.setM(data.get("M").asInt()); + indexInfo.setChecksum(data.get("checksum").asLong()); + indexInfo.setEfCon(data.get("ef_con").asInt()); + + if (data.has("version") && !data.get("version").isNull()) { + indexInfo.setVersion(data.get("version").asInt()); + } + if (data.has("sparse_dim") && !data.get("sparse_dim").isNull()) { + indexInfo.setSparseDimension(data.get("sparse_dim").asInt()); + } + + return new Index(name, token, baseUrl, version, indexInfo); + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to get index", e); + } + } + + private HttpRequest buildGetRequest(String path) { + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "application/json") + .timeout(DEFAULT_TIMEOUT) + .GET(); + + if (token != null && !token.isEmpty()) { + builder.header("Authorization", token); } - private HttpRequest buildPostRequest(String path, Map data) { - String json = JsonUtils.toJson(data); - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + path)) - .header("Content-Type", "application/json") - .timeout(DEFAULT_TIMEOUT) - .POST(HttpRequest.BodyPublishers.ofString(json)); + return builder.build(); + } - if (token != null && !token.isEmpty()) { - builder.header("Authorization", token); - } + private HttpRequest buildPostRequest(String path, Map data) { + String json = JsonUtils.toJson(data); + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "application/json") + .timeout(DEFAULT_TIMEOUT) + .POST(HttpRequest.BodyPublishers.ofString(json)); - return builder.build(); + if (token != null && !token.isEmpty()) { + builder.header("Authorization", token); } - private HttpRequest buildDeleteRequest(String path) { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + path)) - .timeout(DEFAULT_TIMEOUT) - .DELETE(); + return builder.build(); + } - if (token != null && !token.isEmpty()) { - builder.header("Authorization", token); - } + private HttpRequest buildDeleteRequest(String path) { + HttpRequest.Builder builder = + HttpRequest.newBuilder().uri(URI.create(baseUrl + path)).timeout(DEFAULT_TIMEOUT).DELETE(); - return builder.build(); + if (token != null && !token.isEmpty()) { + builder.header("Authorization", token); } + + return builder.build(); + } } diff --git a/src/main/java/io/endee/client/Index.java b/src/main/java/io/endee/client/Index.java index 64f6756..269e36b 100644 --- a/src/main/java/io/endee/client/Index.java +++ b/src/main/java/io/endee/client/Index.java @@ -7,7 +7,6 @@ import io.endee.client.util.JsonUtils; import io.endee.client.util.MessagePackUtils; import io.endee.client.util.ValidationUtils; - import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -20,10 +19,8 @@ /** * Index client for Endee-DB vector operations. * - *

- * Example usage: - *

- * + *

Example usage: + * *

{@code
  * Index index = client.getIndex("my_index");
  *
@@ -43,459 +40,473 @@
  * }
*/ public class Index { - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); - private static final int MAX_BATCH_SIZE = 1000; - private static final int MAX_TOP_K = 512; - private static final int MAX_EF = 1024; - - private final String name; - private final String token; - private final String url; - private final HttpClient httpClient; - - private long count; - private SpaceType spaceType; - private int dimension; - private Precision precision; - private int m; - private int sparseDimension; - - /** - * Creates a new Index instance. - */ - public Index(String name, String token, String url, int version, IndexInfo params) { - this.name = name; - this.token = token; - this.url = url; - - this.count = params != null ? params.getTotalElements() : 0; - this.spaceType = params != null && params.getSpaceType() != null ? params.getSpaceType() : SpaceType.COSINE; - this.dimension = params != null ? params.getDimension() : 0; - this.precision = params != null && params.getPrecision() != null ? params.getPrecision() : Precision.INT16; - this.m = params != null ? params.getM() : 16; - this.sparseDimension = params != null && params.getSparseDimension() != null ? params.getSparseDimension() : 0; - - this.httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .connectTimeout(DEFAULT_TIMEOUT) - .build(); + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); + private static final int MAX_BATCH_SIZE = 1000; + private static final int MAX_TOP_K = 512; + private static final int MAX_EF = 1024; + + private final String name; + private final String token; + private final String url; + private final HttpClient httpClient; + + private long count; + private SpaceType spaceType; + private int dimension; + private Precision precision; + private int m; + private int sparseDimension; + private int efCon; + + /** Creates a new Index instance. */ + public Index(String name, String token, String url, int version, IndexInfo params) { + this.name = name; + this.token = token; + this.url = url; + + this.count = params != null ? params.getTotalElements() : 0; + this.spaceType = + params != null && params.getSpaceType() != null ? params.getSpaceType() : SpaceType.COSINE; + this.dimension = params != null ? params.getDimension() : 0; + this.precision = + params != null && params.getPrecision() != null ? params.getPrecision() : Precision.INT16; + this.m = params != null ? params.getM() : 16; + this.sparseDimension = + params != null && params.getSparseDimension() != null ? params.getSparseDimension() : 0; + this.efCon = params != null ? params.getEfCon() : 128; + + this.httpClient = + HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(DEFAULT_TIMEOUT) + .build(); + } + + @Override + public String toString() { + return name; + } + + /** Checks if this index supports hybrid (sparse + dense) vectors. */ + public boolean isHybrid() { + return sparseDimension > 0; + } + + /** Normalizes a vector for cosine similarity. Returns [normalizedVector, norm]. */ + private double[][] normalizeVector(double[] vector) { + if (vector.length != dimension) { + throw new IllegalArgumentException( + "Vector dimension mismatch: expected " + dimension + ", got " + vector.length); } - @Override - public String toString() { - return name; + if (spaceType != SpaceType.COSINE) { + return new double[][] {vector, {1.0}}; } - /** - * Checks if this index supports hybrid (sparse + dense) vectors. - */ - public boolean isHybrid() { - return sparseDimension > 0; + double sumSquares = 0; + for (double v : vector) { + sumSquares += v * v; } + double norm = Math.sqrt(sumSquares); - /** - * Normalizes a vector for cosine similarity. - * Returns [normalizedVector, norm]. - */ - private double[][] normalizeVector(double[] vector) { - if (vector.length != dimension) { - throw new IllegalArgumentException( - "Vector dimension mismatch: expected " + dimension + ", got " + vector.length); - } - - if (spaceType != SpaceType.COSINE) { - return new double[][] { vector, { 1.0 } }; - } - - double sumSquares = 0; - for (double v : vector) { - sumSquares += v * v; - } - double norm = Math.sqrt(sumSquares); - - if (norm == 0) { - return new double[][] { vector, { 1.0 } }; - } + if (norm == 0) { + return new double[][] {vector, {1.0}}; + } - double[] normalized = new double[vector.length]; - for (int i = 0; i < vector.length; i++) { - normalized[i] = vector[i] / norm; - } + double[] normalized = new double[vector.length]; + for (int i = 0; i < vector.length; i++) { + normalized[i] = vector[i] / norm; + } - return new double[][] { normalized, { norm } }; + return new double[][] {normalized, {norm}}; + } + + /** + * Upserts vectors into the index. + * + * @param inputArray list of vector items to upsert + * @return success message + */ + public String upsert(List inputArray) { + if (inputArray.size() > MAX_BATCH_SIZE) { + throw new IllegalArgumentException( + "Cannot insert more than " + MAX_BATCH_SIZE + " vectors at a time"); } - /** - * Upserts vectors into the index. - * - * @param inputArray list of vector items to upsert - * @return success message - */ - public String upsert(List inputArray) { - if (inputArray.size() > MAX_BATCH_SIZE) { - throw new IllegalArgumentException("Cannot insert more than " + MAX_BATCH_SIZE + " vectors at a time"); - } + List ids = + inputArray.stream() + .map(item -> item.getId() != null ? item.getId() : "") + .collect(Collectors.toList()); + ValidationUtils.validateVectorIds(ids); - List ids = inputArray.stream() - .map(item -> item.getId() != null ? item.getId() : "") - .collect(Collectors.toList()); - ValidationUtils.validateVectorIds(ids); - - List vectorBatch = new ArrayList<>(); - - for (VectorItem item : inputArray) { - double[][] result = normalizeVector(item.getVector()); - double[] normalizedVector = result[0]; - double norm = result[1][0]; - - byte[] metaData = CryptoUtils.jsonZip(item.getMeta() != null ? item.getMeta() : Map.of()); - - int[] sparseIndices = item.getSparseIndices() != null ? item.getSparseIndices() : new int[0]; - double[] sparseValues = item.getSparseValues() != null ? item.getSparseValues() : new double[0]; - - if (!isHybrid() && (sparseIndices.length > 0 || sparseValues.length > 0)) { - throw new IllegalArgumentException( - "Cannot insert sparse data into a dense-only index. Create index with sparseDimension > 0 for hybrid support."); - } - - if (isHybrid()) { - if (sparseIndices.length == 0 || sparseValues.length == 0) { - throw new IllegalArgumentException( - "Both sparse_indices and sparse_values must be provided for hybrid vectors."); - } - if (sparseIndices.length != sparseValues.length) { - throw new IllegalArgumentException( - "sparseIndices and sparseValues must have the same length. Got " + - sparseIndices.length + " indices and " + sparseValues.length + " values."); - } - for (int idx : sparseIndices) { - if (idx < 0 || idx >= sparseDimension) { - throw new IllegalArgumentException( - "Sparse index " + idx + " is out of bounds. Must be in range [0," + sparseDimension - + ")."); - } - } - } - - String filterJson = JsonUtils.toJson(item.getFilter() != null ? item.getFilter() : Map.of()); - - if (isHybrid()) { - vectorBatch.add(new Object[] { item.getId(), metaData, filterJson, norm, normalizedVector, - sparseIndices, sparseValues }); - } else { - vectorBatch.add(new Object[] { item.getId(), metaData, filterJson, norm, normalizedVector }); - } - } + List vectorBatch = new ArrayList<>(); - byte[] serializedData = MessagePackUtils.packVectors(vectorBatch); + for (VectorItem item : inputArray) { + double[][] result = normalizeVector(item.getVector()); + double[] normalizedVector = result[0]; + double norm = result[1][0]; - try { - HttpRequest request = buildPostMsgpackRequest("/index/" + name + "/vector/insert", serializedData); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + byte[] metaData = CryptoUtils.jsonZip(item.getMeta() != null ? item.getMeta() : Map.of()); - if (response.statusCode() != 200) { - EndeeApiException.raiseException(response.statusCode(), new String(response.body())); - } + int[] sparseIndices = item.getSparseIndices() != null ? item.getSparseIndices() : new int[0]; + double[] sparseValues = + item.getSparseValues() != null ? item.getSparseValues() : new double[0]; - return "Vectors inserted successfully"; - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to upsert vectors", e); - } - } + if (!isHybrid() && (sparseIndices.length > 0 || sparseValues.length > 0)) { + throw new IllegalArgumentException( + "Cannot insert sparse data into a dense-only index. Create index with sparseDimension > 0 for hybrid support."); + } - /** - * Queries the index for similar vectors. - * - * @param options the query options - * @return list of query results - */ - public List query(QueryOptions options) { - if (options.getTopK() > MAX_TOP_K || options.getTopK() < 0) { - throw new IllegalArgumentException("top_k cannot be greater than " + MAX_TOP_K + " and less than 0"); - } - if (options.getEf() > MAX_EF) { - throw new IllegalArgumentException("ef search cannot be greater than " + MAX_EF); + if (isHybrid()) { + if (sparseIndices.length == 0 || sparseValues.length == 0) { + throw new IllegalArgumentException( + "Both sparse_indices and sparse_values must be provided for hybrid vectors."); } - if (options.getPrefilterCardinalityThreshold() < 1_000 || options.getPrefilterCardinalityThreshold() > 1_000_000) { - throw new IllegalArgumentException("prefilterCardinalityThreshold must be between 1,000 and 1,000,000"); + if (sparseIndices.length != sparseValues.length) { + throw new IllegalArgumentException( + "sparseIndices and sparseValues must have the same length. Got " + + sparseIndices.length + + " indices and " + + sparseValues.length + + " values."); } - if (options.getFilterBoostPercentage() < 0 || options.getFilterBoostPercentage() > 100) { - throw new IllegalArgumentException("filterBoostPercentage must be between 0 and 100"); - } - - boolean hasSparse = options.getSparseIndices() != null && options.getSparseIndices().length > 0 - && options.getSparseValues() != null && options.getSparseValues().length > 0; - boolean hasDense = options.getVector() != null; - - if (!hasDense && !hasSparse) { + for (int idx : sparseIndices) { + if (idx < 0 || idx >= sparseDimension) { throw new IllegalArgumentException( - "At least one of 'vector' or 'sparseIndices'/'sparseValues' must be provided."); - } - - if (hasSparse && !isHybrid()) { - throw new IllegalArgumentException( - "Cannot perform sparse search on a dense-only index. Create index with sparseDimension > 0 for hybrid support."); + "Sparse index " + + idx + + " is out of bounds. Must be in range [0," + + sparseDimension + + ")."); + } } + } + + String filterJson = JsonUtils.toJson(item.getFilter() != null ? item.getFilter() : Map.of()); + + if (isHybrid()) { + vectorBatch.add( + new Object[] { + item.getId(), + metaData, + filterJson, + norm, + normalizedVector, + sparseIndices, + sparseValues + }); + } else { + vectorBatch.add(new Object[] {item.getId(), metaData, filterJson, norm, normalizedVector}); + } + } - if (hasSparse && options.getSparseIndices().length != options.getSparseValues().length) { - throw new IllegalArgumentException( - "sparseIndices and sparseValues must have the same length."); - } + byte[] serializedData = MessagePackUtils.packVectors(vectorBatch); - Map data = new HashMap<>(); - data.put("k", options.getTopK()); - data.put("ef", options.getEf()); - data.put("include_vectors", options.isIncludeVectors()); + try { + HttpRequest request = + buildPostMsgpackRequest("/index/" + name + "/vector/insert", serializedData); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); - if (hasDense) { - double[][] result = normalizeVector(options.getVector()); - data.put("vector", result[0]); - } + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), new String(response.body())); + } - if (hasSparse) { - data.put("sparse_indices", options.getSparseIndices()); - data.put("sparse_values", options.getSparseValues()); - } + return "Vectors inserted successfully"; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to upsert vectors", e); + } + } + + /** + * Queries the index for similar vectors. + * + * @param options the query options + * @return list of query results + */ + public List query(QueryOptions options) { + if (options.getTopK() > MAX_TOP_K || options.getTopK() < 0) { + throw new IllegalArgumentException( + "top_k cannot be greater than " + MAX_TOP_K + " and less than 0"); + } + if (options.getEf() > MAX_EF) { + throw new IllegalArgumentException("ef search cannot be greater than " + MAX_EF); + } + if (options.getPrefilterCardinalityThreshold() < 1_000 + || options.getPrefilterCardinalityThreshold() > 1_000_000) { + throw new IllegalArgumentException( + "prefilterCardinalityThreshold must be between 1,000 and 1,000,000"); + } + if (options.getFilterBoostPercentage() < 0 || options.getFilterBoostPercentage() > 100) { + throw new IllegalArgumentException("filterBoostPercentage must be between 0 and 100"); + } - if (options.getFilter() != null) { - data.put("filter", JsonUtils.toJson(options.getFilter())); - } + boolean hasSparse = + options.getSparseIndices() != null + && options.getSparseIndices().length > 0 + && options.getSparseValues() != null + && options.getSparseValues().length > 0; + boolean hasDense = options.getVector() != null; - Map filterParams = new HashMap<>(); - filterParams.put("prefilter_cardinality_threshold", options.getPrefilterCardinalityThreshold()); - filterParams.put("filter_boost_percentage", options.getFilterBoostPercentage()); - data.put("filter_params", filterParams); - - try { - String jsonBody = JsonUtils.toJson(data); - HttpRequest request = buildPostJsonRequest("/index/" + name + "/search", jsonBody); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); - - if (response.statusCode() != 200) { - EndeeApiException.raiseException(response.statusCode(), new String(response.body())); - } - - List decoded = MessagePackUtils.unpackQueryResults(response.body()); - List results = new ArrayList<>(); - - for (Object[] tuple : decoded) { - double similarity = (Double) tuple[0]; - String vectorId = (String) tuple[1]; - byte[] metaData = (byte[]) tuple[2]; - String filterStr = (String) tuple[3]; - double normValue = (Double) tuple[4]; - - Map meta = CryptoUtils.jsonUnzip(metaData); - - QueryResult result = new QueryResult(); - result.setId(vectorId); - result.setSimilarity(similarity); - result.setDistance(1 - similarity); - result.setMeta(meta); - result.setNorm(normValue); - - if (filterStr != null && !filterStr.isEmpty() && !filterStr.equals("{}")) { - @SuppressWarnings("unchecked") - Map parsedFilter = JsonUtils.fromJson(filterStr, Map.class); - result.setFilter(parsedFilter); - } - - if (options.isIncludeVectors() && tuple.length > 5) { - result.setVector((double[]) tuple[5]); - } - - results.add(result); - } - - return results; - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to query index", e); - } + if (!hasDense && !hasSparse) { + throw new IllegalArgumentException( + "At least one of 'vector' or 'sparseIndices'/'sparseValues' must be provided."); } - /** - * Deletes a vector by ID. - * - * @param id the vector ID to delete - * @return success message - */ - public String deleteVector(String id) { - try { - HttpRequest request = buildDeleteRequest("/index/" + name + "/vector/" + id + "/delete"); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - EndeeApiException.raiseException(response.statusCode(), response.body()); - } - - return response.body(); - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to delete vector", e); - } + if (hasSparse && !isHybrid()) { + throw new IllegalArgumentException( + "Cannot perform sparse search on a dense-only index. Create index with sparseDimension > 0 for hybrid support."); } - /** - * Deletes vectors matching a filter. - * - * @param filter the filter criteria - * @return the API response - */ - public String deleteWithFilter(List> filter) { - try { - Map data = Map.of("filter", filter); - String jsonBody = JsonUtils.toJson(data); - - HttpRequest request = buildDeleteJsonRequest("/index/" + name + "/vectors/delete", jsonBody); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - EndeeApiException.raiseException(response.statusCode(), response.body()); - } - - return response.body(); - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to delete vectors with filter", e); - } + if (hasSparse && options.getSparseIndices().length != options.getSparseValues().length) { + throw new IllegalArgumentException( + "sparseIndices and sparseValues must have the same length."); } - /** - * Gets a vector by ID. - * - * @param id the vector ID - * @return the vector information - */ - public VectorInfo getVector(String id) { - try { - Map data = Map.of("id", id); - String jsonBody = JsonUtils.toJson(data); - - HttpRequest request = buildPostJsonRequest("/index/" + name + "/vector/get", jsonBody); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); - - if (response.statusCode() != 200) { - EndeeApiException.raiseException(response.statusCode(), new String(response.body())); - } - - Object[] vectorObj = MessagePackUtils.unpackVector(response.body()); - - VectorInfo info = new VectorInfo(); - info.setId((String) vectorObj[0]); - info.setMeta(CryptoUtils.jsonUnzip((byte[]) vectorObj[1])); - - String filterStr = (String) vectorObj[2]; - if (filterStr != null && !filterStr.isEmpty() && !filterStr.equals("{}")) { - @SuppressWarnings("unchecked") - Map parsedFilter = JsonUtils.fromJson(filterStr, Map.class); - info.setFilter(parsedFilter); - } - - info.setNorm((Double) vectorObj[3]); - info.setVector((double[]) vectorObj[4]); - - return info; - } catch (IOException | InterruptedException e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw new EndeeException("Failed to get vector", e); - } + Map data = new HashMap<>(); + data.put("k", options.getTopK()); + data.put("ef", options.getEf()); + data.put("include_vectors", options.isIncludeVectors()); + + if (hasDense) { + double[][] result = normalizeVector(options.getVector()); + data.put("vector", result[0]); } - /** - * Returns a description of this index. - * - * @return the index description - */ - public IndexDescription describe() { - return new IndexDescription( - name, - spaceType, - dimension, - sparseDimension, - isHybrid(), - count, - precision, - m); + if (hasSparse) { + data.put("sparse_indices", options.getSparseIndices()); + data.put("sparse_values", options.getSparseValues()); } - // ==================== HTTP Request Helper Methods ==================== + if (options.getFilter() != null) { + data.put("filter", JsonUtils.toJson(options.getFilter())); + } - /** - * Builds a POST request with JSON body. - */ - private HttpRequest buildPostJsonRequest(String path, String jsonBody) { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(url + path)) - .header("Content-Type", "application/json") - .timeout(DEFAULT_TIMEOUT) - .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + Map filterParams = new HashMap<>(); + filterParams.put("prefilter_cardinality_threshold", options.getPrefilterCardinalityThreshold()); + filterParams.put("filter_boost_percentage", options.getFilterBoostPercentage()); + data.put("filter_params", filterParams); + + try { + String jsonBody = JsonUtils.toJson(data); + HttpRequest request = buildPostJsonRequest("/index/" + name + "/search", jsonBody); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), new String(response.body())); + } + + List decoded = MessagePackUtils.unpackQueryResults(response.body()); + List results = new ArrayList<>(); + + for (Object[] tuple : decoded) { + double similarity = (Double) tuple[0]; + String vectorId = (String) tuple[1]; + byte[] metaData = (byte[]) tuple[2]; + String filterStr = (String) tuple[3]; + double normValue = (Double) tuple[4]; + + Map meta = CryptoUtils.jsonUnzip(metaData); + + QueryResult result = new QueryResult(); + result.setId(vectorId); + result.setSimilarity(similarity); + result.setDistance(1 - similarity); + result.setMeta(meta); + result.setNorm(normValue); + + if (filterStr != null && !filterStr.isEmpty() && !filterStr.equals("{}")) { + @SuppressWarnings("unchecked") + Map parsedFilter = JsonUtils.fromJson(filterStr, Map.class); + result.setFilter(parsedFilter); + } - if (token != null && !token.isBlank()) { - builder.header("Authorization", token); + if (options.isIncludeVectors() && tuple.length > 5) { + result.setVector((double[]) tuple[5]); } - return builder.build(); + results.add(result); + } + + return results; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to query index", e); + } + } + + /** + * Deletes a vector by ID. + * + * @param id the vector ID to delete + * @return success message + */ + public String deleteVector(String id) { + try { + HttpRequest request = buildDeleteRequest("/index/" + name + "/vector/" + id + "/delete"); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), response.body()); + } + + return response.body(); + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to delete vector", e); + } + } + + /** + * Deletes vectors matching a filter. + * + * @param filter the filter criteria + * @return the API response + */ + public String deleteWithFilter(List> filter) { + try { + Map data = Map.of("filter", filter); + String jsonBody = JsonUtils.toJson(data); + + HttpRequest request = buildDeleteJsonRequest("/index/" + name + "/vectors/delete", jsonBody); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), response.body()); + } + + return response.body(); + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to delete vectors with filter", e); + } + } + + /** + * Gets a vector by ID. + * + * @param id the vector ID + * @return the vector information + */ + public VectorInfo getVector(String id) { + try { + Map data = Map.of("id", id); + String jsonBody = JsonUtils.toJson(data); + + HttpRequest request = buildPostJsonRequest("/index/" + name + "/vector/get", jsonBody); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), new String(response.body())); + } + + Object[] vectorObj = MessagePackUtils.unpackVector(response.body()); + + VectorInfo info = new VectorInfo(); + info.setId((String) vectorObj[0]); + info.setMeta(CryptoUtils.jsonUnzip((byte[]) vectorObj[1])); + + String filterStr = (String) vectorObj[2]; + if (filterStr != null && !filterStr.isEmpty() && !filterStr.equals("{}")) { + @SuppressWarnings("unchecked") + Map parsedFilter = JsonUtils.fromJson(filterStr, Map.class); + info.setFilter(parsedFilter); + } + + info.setNorm((Double) vectorObj[3]); + info.setVector((double[]) vectorObj[4]); + + return info; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to get vector", e); + } + } + + /** + * Returns a description of this index. + * + * @return the index description + */ + public IndexDescription describe() { + return new IndexDescription( + name, spaceType, dimension, sparseDimension, isHybrid(), count, precision, m, efCon); + } + + // ==================== HTTP Request Helper Methods ==================== + + /** Builds a POST request with JSON body. */ + private HttpRequest buildPostJsonRequest(String path, String jsonBody) { + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create(url + path)) + .header("Content-Type", "application/json") + .timeout(DEFAULT_TIMEOUT) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + + if (token != null && !token.isBlank()) { + builder.header("Authorization", token); } - /** - * Builds a POST request with MessagePack body. - */ - private HttpRequest buildPostMsgpackRequest(String path, byte[] body) { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(url + path)) - .header("Content-Type", "application/msgpack") - .timeout(DEFAULT_TIMEOUT) - .POST(HttpRequest.BodyPublishers.ofByteArray(body)); - - if (token != null && !token.isBlank()) { - builder.header("Authorization", token); - } + return builder.build(); + } - return builder.build(); + /** Builds a POST request with MessagePack body. */ + private HttpRequest buildPostMsgpackRequest(String path, byte[] body) { + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create(url + path)) + .header("Content-Type", "application/msgpack") + .timeout(DEFAULT_TIMEOUT) + .POST(HttpRequest.BodyPublishers.ofByteArray(body)); + + if (token != null && !token.isBlank()) { + builder.header("Authorization", token); } - /** - * Builds a DELETE request. - */ - private HttpRequest buildDeleteRequest(String path) { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(url + path)) - .timeout(DEFAULT_TIMEOUT) - .DELETE(); - - if (token != null && !token.isBlank()) { - builder.header("Authorization", token); - } + return builder.build(); + } + + /** Builds a DELETE request. */ + private HttpRequest buildDeleteRequest(String path) { + HttpRequest.Builder builder = + HttpRequest.newBuilder().uri(URI.create(url + path)).timeout(DEFAULT_TIMEOUT).DELETE(); - return builder.build(); + if (token != null && !token.isBlank()) { + builder.header("Authorization", token); } - /** - * Builds a DELETE request with JSON body. - */ - private HttpRequest buildDeleteJsonRequest(String path, String jsonBody) { - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(url + path)) - .header("Content-Type", "application/json") - .timeout(DEFAULT_TIMEOUT) - .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonBody)); - - if (token != null && !token.isBlank()) { - builder.header("Authorization", token); - } + return builder.build(); + } + + /** Builds a DELETE request with JSON body. */ + private HttpRequest buildDeleteJsonRequest(String path, String jsonBody) { + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create(url + path)) + .header("Content-Type", "application/json") + .timeout(DEFAULT_TIMEOUT) + .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonBody)); - return builder.build(); + if (token != null && !token.isBlank()) { + builder.header("Authorization", token); } + + return builder.build(); + } } diff --git a/src/main/java/io/endee/client/exception/EndeeApiException.java b/src/main/java/io/endee/client/exception/EndeeApiException.java index 1a6ab68..70bcb11 100644 --- a/src/main/java/io/endee/client/exception/EndeeApiException.java +++ b/src/main/java/io/endee/client/exception/EndeeApiException.java @@ -1,40 +1,37 @@ package io.endee.client.exception; -/** - * Exception thrown when the Endee API returns an error response. - */ +/** Exception thrown when the Endee API returns an error response. */ public class EndeeApiException extends EndeeException { - private final int statusCode; - private final String errorBody; + private final int statusCode; + private final String errorBody; - public EndeeApiException(String message, int statusCode, String errorBody) { - super(message); - this.statusCode = statusCode; - this.errorBody = errorBody; - } + public EndeeApiException(String message, int statusCode, String errorBody) { + super(message); + this.statusCode = statusCode; + this.errorBody = errorBody; + } - public int getStatusCode() { - return statusCode; - } + public int getStatusCode() { + return statusCode; + } - public String getErrorBody() { - return errorBody; - } + public String getErrorBody() { + return errorBody; + } - /** - * Raises the appropriate exception based on status code. - */ - public static void raiseException(int statusCode, String errorBody) { - String message = switch (statusCode) { - case 400 -> "Bad Request: " + errorBody; - case 401 -> "Unauthorized: " + errorBody; - case 403 -> "Forbidden: " + errorBody; - case 404 -> "Not Found: " + errorBody; - case 409 -> "Conflict: " + errorBody; - case 500 -> "Internal Server Error: " + errorBody; - default -> "API Error (" + statusCode + "): " + errorBody; + /** Raises the appropriate exception based on status code. */ + public static void raiseException(int statusCode, String errorBody) { + String message = + switch (statusCode) { + case 400 -> "Bad Request: " + errorBody; + case 401 -> "Unauthorized: " + errorBody; + case 403 -> "Forbidden: " + errorBody; + case 404 -> "Not Found: " + errorBody; + case 409 -> "Conflict: " + errorBody; + case 500 -> "Internal Server Error: " + errorBody; + default -> "API Error (" + statusCode + "): " + errorBody; }; - throw new EndeeApiException(message, statusCode, errorBody); - } + throw new EndeeApiException(message, statusCode, errorBody); + } } diff --git a/src/main/java/io/endee/client/exception/EndeeException.java b/src/main/java/io/endee/client/exception/EndeeException.java index ea56c96..fe9af5d 100644 --- a/src/main/java/io/endee/client/exception/EndeeException.java +++ b/src/main/java/io/endee/client/exception/EndeeException.java @@ -1,19 +1,17 @@ package io.endee.client.exception; -/** - * Base exception for all Endee client errors. - */ +/** Base exception for all Endee client errors. */ public class EndeeException extends RuntimeException { - public EndeeException(String message) { - super(message); - } + public EndeeException(String message) { + super(message); + } - public EndeeException(String message, Throwable cause) { - super(message, cause); - } + public EndeeException(String message, Throwable cause) { + super(message, cause); + } - public EndeeException(Throwable cause) { - super(cause); - } + public EndeeException(Throwable cause) { + super(cause); + } } diff --git a/src/main/java/io/endee/client/types/CreateIndexOptions.java b/src/main/java/io/endee/client/types/CreateIndexOptions.java index f03979b..83b204a 100644 --- a/src/main/java/io/endee/client/types/CreateIndexOptions.java +++ b/src/main/java/io/endee/client/types/CreateIndexOptions.java @@ -1,75 +1,96 @@ package io.endee.client.types; -/** - * Options for creating an Endee index. - */ +/** Options for creating an Endee index. */ public class CreateIndexOptions { - private final String name; - private final int dimension; - private SpaceType spaceType = SpaceType.COSINE; - private int m = 16; - private int efCon = 128; - private Precision precision = Precision.INT16; - private Integer version = null; - private Integer sparseDimension = null; - - private CreateIndexOptions(String name, int dimension) { - this.name = name; - this.dimension = dimension; + private final String name; + private final int dimension; + private SpaceType spaceType = SpaceType.COSINE; + private int m = 16; + private int efCon = 128; + private Precision precision = Precision.INT16; + private Integer version = null; + private Integer sparseDimension = null; + + private CreateIndexOptions(String name, int dimension) { + this.name = name; + this.dimension = dimension; + } + + public static Builder builder(String name, int dimension) { + return new Builder(name, dimension); + } + + public String getName() { + return name; + } + + public int getDimension() { + return dimension; + } + + public SpaceType getSpaceType() { + return spaceType; + } + + public int getM() { + return m; + } + + public int getEfCon() { + return efCon; + } + + public Precision getPrecision() { + return precision; + } + + public Integer getVersion() { + return version; + } + + public Integer getSparseDimension() { + return sparseDimension; + } + + public static class Builder { + private final CreateIndexOptions options; + + private Builder(String name, int dimension) { + this.options = new CreateIndexOptions(name, dimension); + } + + public Builder spaceType(SpaceType spaceType) { + options.spaceType = spaceType; + return this; + } + + public Builder m(int m) { + options.m = m; + return this; + } + + public Builder efCon(int efCon) { + options.efCon = efCon; + return this; + } + + public Builder precision(Precision precision) { + options.precision = precision; + return this; + } + + public Builder version(Integer version) { + options.version = version; + return this; } - public static Builder builder(String name, int dimension) { - return new Builder(name, dimension); + public Builder sparseDimension(Integer sparseDimension) { + options.sparseDimension = sparseDimension; + return this; } - public String getName() { return name; } - public int getDimension() { return dimension; } - public SpaceType getSpaceType() { return spaceType; } - public int getM() { return m; } - public int getEfCon() { return efCon; } - public Precision getPrecision() { return precision; } - public Integer getVersion() { return version; } - public Integer getSparseDimension() { return sparseDimension; } - - public static class Builder { - private final CreateIndexOptions options; - - private Builder(String name, int dimension) { - this.options = new CreateIndexOptions(name, dimension); - } - - public Builder spaceType(SpaceType spaceType) { - options.spaceType = spaceType; - return this; - } - - public Builder m(int m) { - options.m = m; - return this; - } - - public Builder efCon(int efCon) { - options.efCon = efCon; - return this; - } - - public Builder precision(Precision precision) { - options.precision = precision; - return this; - } - - public Builder version(Integer version) { - options.version = version; - return this; - } - - public Builder sparseDimension(Integer sparseDimension) { - options.sparseDimension = sparseDimension; - return this; - } - - public CreateIndexOptions build() { - return options; - } + public CreateIndexOptions build() { + return options; } + } } diff --git a/src/main/java/io/endee/client/types/IndexDescription.java b/src/main/java/io/endee/client/types/IndexDescription.java index 045b96d..02bfeac 100644 --- a/src/main/java/io/endee/client/types/IndexDescription.java +++ b/src/main/java/io/endee/client/types/IndexDescription.java @@ -1,43 +1,94 @@ package io.endee.client.types; -/** - * Description of an Endee index. - */ +/** Description of an Endee index. */ public class IndexDescription { - private final String name; - private final SpaceType spaceType; - private final int dimension; - private final int sparseDimension; - private final boolean isHybrid; - private final long count; - private final Precision precision; - private final int m; - - public IndexDescription(String name, SpaceType spaceType, int dimension, - int sparseDimension, boolean isHybrid, long count, - Precision precision, int m) { - this.name = name; - this.spaceType = spaceType; - this.dimension = dimension; - this.sparseDimension = sparseDimension; - this.isHybrid = isHybrid; - this.count = count; - this.precision = precision; - this.m = m; - } - - public String getName() { return name; } - public SpaceType getSpaceType() { return spaceType; } - public int getDimension() { return dimension; } - public int getSparseDimension() { return sparseDimension; } - public boolean isHybrid() { return isHybrid; } - public long getCount() { return count; } - public Precision getPrecision() { return precision; } - public int getM() { return m; } - - @Override - public String toString() { - return "{name='" + name + "', spaceType= " + spaceType + - ", dimension=" + dimension + ", precision=" + precision + ", count=" + count + ", isHybrid=" + isHybrid +", sparseDimension=" + sparseDimension + ", M=" + m +"}"; - } + private final String name; + private final SpaceType spaceType; + private final int dimension; + private final int sparseDimension; + private final boolean isHybrid; + private final long count; + private final Precision precision; + private final int m; + private final int efCon; + + public IndexDescription( + String name, + SpaceType spaceType, + int dimension, + int sparseDimension, + boolean isHybrid, + long count, + Precision precision, + int m, + int efCon) { + this.name = name; + this.spaceType = spaceType; + this.dimension = dimension; + this.sparseDimension = sparseDimension; + this.isHybrid = isHybrid; + this.count = count; + this.precision = precision; + this.m = m; + this.efCon = efCon; + } + + public String getName() { + return name; + } + + public SpaceType getSpaceType() { + return spaceType; + } + + public int getDimension() { + return dimension; + } + + public int getSparseDimension() { + return sparseDimension; + } + + public boolean isHybrid() { + return isHybrid; + } + + public long getCount() { + return count; + } + + public Precision getPrecision() { + return precision; + } + + public int getM() { + return m; + } + + public int getEfCon() { + return efCon; + } + + @Override + public String toString() { + return "{name='" + + name + + "', spaceType= " + + spaceType + + ", dimension=" + + dimension + + ", precision=" + + precision + + ", count=" + + count + + ", isHybrid=" + + isHybrid + + ", sparseDimension=" + + sparseDimension + + ", M=" + + m + + ", efCon=" + + efCon + + "}"; + } } diff --git a/src/main/java/io/endee/client/types/IndexInfo.java b/src/main/java/io/endee/client/types/IndexInfo.java index f732c44..5fbd8df 100644 --- a/src/main/java/io/endee/client/types/IndexInfo.java +++ b/src/main/java/io/endee/client/types/IndexInfo.java @@ -1,45 +1,97 @@ package io.endee.client.types; -/** - * Information about an Endee index from the server. - */ +/** Information about an Endee index from the server. */ public class IndexInfo { - private String name; - private SpaceType spaceType; - private int dimension; - private long totalElements; - private Precision precision; - private int m; - private long checksum; - private Integer version; - private Integer sparseDimension; + private String name; + private SpaceType spaceType; + private int dimension; + private long totalElements; + private Precision precision; + private int m; + private long checksum; + private int version; + private Integer sparseDimension; + private int efCon; - public IndexInfo() {} + public IndexInfo() {} - public String getName() { return name; } - public void setName(String name) { this.name = name; } + public String getName() { + return name; + } - public SpaceType getSpaceType() { return spaceType; } - public void setSpaceType(SpaceType spaceType) { this.spaceType = spaceType; } + public void setName(String name) { + this.name = name; + } - public int getDimension() { return dimension; } - public void setDimension(int dimension) { this.dimension = dimension; } + public SpaceType getSpaceType() { + return spaceType; + } - public long getTotalElements() { return totalElements; } - public void setTotalElements(long totalElements) { this.totalElements = totalElements; } + public void setSpaceType(SpaceType spaceType) { + this.spaceType = spaceType; + } - public Precision getPrecision() { return precision; } - public void setPrecision(Precision precision) { this.precision = precision; } + public int getDimension() { + return dimension; + } - public int getM() { return m; } - public void setM(int m) { this.m = m; } + public void setDimension(int dimension) { + this.dimension = dimension; + } - public long getChecksum() { return checksum; } - public void setChecksum(long checksum) { this.checksum = checksum; } + public long getTotalElements() { + return totalElements; + } - public Integer getVersion() { return version; } - public void setVersion(Integer version) { this.version = version; } + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } - public Integer getSparseDimension() { return sparseDimension; } - public void setSparseDimension(Integer sparseDimension) { this.sparseDimension = sparseDimension; } + public Precision getPrecision() { + return precision; + } + + public void setPrecision(Precision precision) { + this.precision = precision; + } + + public int getM() { + return m; + } + + public void setM(int m) { + this.m = m; + } + + public long getChecksum() { + return checksum; + } + + public void setChecksum(long checksum) { + this.checksum = checksum; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public Integer getSparseDimension() { + return sparseDimension; + } + + public void setSparseDimension(Integer sparseDimension) { + this.sparseDimension = sparseDimension; + } + + public int getEfCon() { + return efCon; + } + + public void setEfCon(int efCon) { + this.efCon = efCon; + } } diff --git a/src/main/java/io/endee/client/types/Precision.java b/src/main/java/io/endee/client/types/Precision.java index cf30495..7178b71 100644 --- a/src/main/java/io/endee/client/types/Precision.java +++ b/src/main/java/io/endee/client/types/Precision.java @@ -1,36 +1,34 @@ package io.endee.client.types; -/** - * Precision types for vector quantization. - */ +/** Precision types for vector quantization. */ public enum Precision { - BINARY("binary"), - INT8("int8"), - INT16("int16"), - FLOAT32("float32"), - FLOAT16("float16"); + BINARY("binary"), + INT8("int8"), + INT16("int16"), + FLOAT32("float32"), + FLOAT16("float16"); - private final String value; + private final String value; - Precision(String value) { - this.value = value; - } + Precision(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static Precision fromValue(String value) { - for (Precision p : values()) { - if (p.value.equalsIgnoreCase(value)) { - return p; - } - } - throw new IllegalArgumentException("Unknown precision: " + value); + public static Precision fromValue(String value) { + for (Precision p : values()) { + if (p.value.equalsIgnoreCase(value)) { + return p; + } } + throw new IllegalArgumentException("Unknown precision: " + value); + } - @Override - public String toString() { - return value; - } + @Override + public String toString() { + return value; + } } diff --git a/src/main/java/io/endee/client/types/QueryOptions.java b/src/main/java/io/endee/client/types/QueryOptions.java index b687fb1..54d798d 100644 --- a/src/main/java/io/endee/client/types/QueryOptions.java +++ b/src/main/java/io/endee/client/types/QueryOptions.java @@ -6,7 +6,8 @@ /** * Options for querying an Endee index. * - *

Example usage with filters:

+ *

Example usage with filters: + * *

{@code
  * QueryOptions options = QueryOptions.builder()
  *     .vector(new double[]{0.1, 0.2, 0.3})
@@ -19,100 +20,126 @@
  * }
*/ public class QueryOptions { - private static final int DEFAULT_PREFILTER_CARDINALITY_THRESHOLD = 10_000; - - private double[] vector; - private int topK; - private List> filter; - private int ef = 128; - private boolean includeVectors = false; - private int[] sparseIndices; - private double[] sparseValues; - private int prefilterCardinalityThreshold = DEFAULT_PREFILTER_CARDINALITY_THRESHOLD; - private int filterBoostPercentage = 0; - - private QueryOptions() {} - - public static Builder builder() { - return new Builder(); + private static final int DEFAULT_PREFILTER_CARDINALITY_THRESHOLD = 10_000; + + private double[] vector; + private int topK; + private List> filter; + private int ef = 128; + private boolean includeVectors = false; + private int[] sparseIndices; + private double[] sparseValues; + private int prefilterCardinalityThreshold = DEFAULT_PREFILTER_CARDINALITY_THRESHOLD; + private int filterBoostPercentage = 0; + + private QueryOptions() {} + + public static Builder builder() { + return new Builder(); + } + + public double[] getVector() { + return vector; + } + + public int getTopK() { + return topK; + } + + public List> getFilter() { + return filter; + } + + public int getEf() { + return ef; + } + + public boolean isIncludeVectors() { + return includeVectors; + } + + public int[] getSparseIndices() { + return sparseIndices; + } + + public double[] getSparseValues() { + return sparseValues; + } + + public int getPrefilterCardinalityThreshold() { + return prefilterCardinalityThreshold; + } + + public int getFilterBoostPercentage() { + return filterBoostPercentage; + } + + public static class Builder { + private final QueryOptions options = new QueryOptions(); + + public Builder vector(double[] vector) { + options.vector = vector; + return this; + } + + public Builder topK(int topK) { + options.topK = topK; + return this; + } + + /** + * Sets the filter conditions as an array of filter objects. + * + * @param filter list of filter conditions, e.g.: [{"category": {"$eq": "tech"}}, {"score": + * {"$range": [80, 100]}}] + * @return this builder + */ + public Builder filter(List> filter) { + options.filter = filter; + return this; + } + + public Builder ef(int ef) { + options.ef = ef; + return this; + } + + public Builder includeVectors(boolean includeVectors) { + options.includeVectors = includeVectors; + return this; + } + + public Builder sparseIndices(int[] sparseIndices) { + options.sparseIndices = sparseIndices; + return this; + } + + public Builder sparseValues(double[] sparseValues) { + options.sparseValues = sparseValues; + return this; + } + + /** + * Sets the prefilter cardinality threshold. When the estimated number of matching vectors + * exceeds this value, postfiltering is used instead. Must be between 1,000 and 1,000,000. + * Default: 10,000. + */ + public Builder prefilterCardinalityThreshold(int prefilterCardinalityThreshold) { + options.prefilterCardinalityThreshold = prefilterCardinalityThreshold; + return this; + } + + /** + * Sets the filter boost percentage (0-100). Higher values bias results toward filter matches. + * Default: 0. + */ + public Builder filterBoostPercentage(int filterBoostPercentage) { + options.filterBoostPercentage = filterBoostPercentage; + return this; } - public double[] getVector() { return vector; } - public int getTopK() { return topK; } - public List> getFilter() { return filter; } - public int getEf() { return ef; } - public boolean isIncludeVectors() { return includeVectors; } - public int[] getSparseIndices() { return sparseIndices; } - public double[] getSparseValues() { return sparseValues; } - public int getPrefilterCardinalityThreshold() { return prefilterCardinalityThreshold; } - public int getFilterBoostPercentage() { return filterBoostPercentage; } - - public static class Builder { - private final QueryOptions options = new QueryOptions(); - - public Builder vector(double[] vector) { - options.vector = vector; - return this; - } - - public Builder topK(int topK) { - options.topK = topK; - return this; - } - - /** - * Sets the filter conditions as an array of filter objects. - * - * @param filter list of filter conditions, e.g.: - * [{"category": {"$eq": "tech"}}, {"score": {"$range": [80, 100]}}] - * @return this builder - */ - public Builder filter(List> filter) { - options.filter = filter; - return this; - } - - public Builder ef(int ef) { - options.ef = ef; - return this; - } - - public Builder includeVectors(boolean includeVectors) { - options.includeVectors = includeVectors; - return this; - } - - public Builder sparseIndices(int[] sparseIndices) { - options.sparseIndices = sparseIndices; - return this; - } - - public Builder sparseValues(double[] sparseValues) { - options.sparseValues = sparseValues; - return this; - } - - /** - * Sets the prefilter cardinality threshold. When the estimated number of - * matching vectors exceeds this value, postfiltering is used instead. - * Must be between 1,000 and 1,000,000. Default: 10,000. - */ - public Builder prefilterCardinalityThreshold(int prefilterCardinalityThreshold) { - options.prefilterCardinalityThreshold = prefilterCardinalityThreshold; - return this; - } - - /** - * Sets the filter boost percentage (0-100). Higher values bias results - * toward filter matches. Default: 0. - */ - public Builder filterBoostPercentage(int filterBoostPercentage) { - options.filterBoostPercentage = filterBoostPercentage; - return this; - } - - public QueryOptions build() { - return options; - } + public QueryOptions build() { + return options; } + } } diff --git a/src/main/java/io/endee/client/types/QueryResult.java b/src/main/java/io/endee/client/types/QueryResult.java index 9b27d68..4f5bcab 100644 --- a/src/main/java/io/endee/client/types/QueryResult.java +++ b/src/main/java/io/endee/client/types/QueryResult.java @@ -3,47 +3,81 @@ import java.util.Arrays; import java.util.Map; -/** - * Result from a query operation. - */ +/** Result from a query operation. */ public class QueryResult { - private String id; - private double similarity; - private double distance; - private Map meta; - private double norm; - private Map filter; - private double[] vector; + private String id; + private double similarity; + private double distance; + private Map meta; + private double norm; + private Map filter; + private double[] vector; - public QueryResult() {} + public QueryResult() {} - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public double getSimilarity() { return similarity; } - public void setSimilarity(double similarity) { this.similarity = similarity; } + public void setId(String id) { + this.id = id; + } - public double getDistance() { return distance; } - public void setDistance(double distance) { this.distance = distance; } + public double getSimilarity() { + return similarity; + } - public Map getMeta() { return meta; } - public void setMeta(Map meta) { this.meta = meta; } + public void setSimilarity(double similarity) { + this.similarity = similarity; + } - public double getNorm() { return norm; } - public void setNorm(double norm) { this.norm = norm; } + public double getDistance() { + return distance; + } - public Map getFilter() { return filter; } - public void setFilter(Map filter) { this.filter = filter; } + public void setDistance(double distance) { + this.distance = distance; + } - public double[] getVector() { return vector; } - public void setVector(double[] vector) { this.vector = vector; } + public Map getMeta() { + return meta; + } - @Override - public String toString() { - String result = "QueryResult{id='" + id + "', similarity=" + similarity + ", distance=" + distance; - if (vector != null) { - result += ", vector=" + Arrays.toString(vector); - } - return result + "}"; + public void setMeta(Map meta) { + this.meta = meta; + } + + public double getNorm() { + return norm; + } + + public void setNorm(double norm) { + this.norm = norm; + } + + public Map getFilter() { + return filter; + } + + public void setFilter(Map filter) { + this.filter = filter; + } + + public double[] getVector() { + return vector; + } + + public void setVector(double[] vector) { + this.vector = vector; + } + + @Override + public String toString() { + String result = + "QueryResult{id='" + id + "', similarity=" + similarity + ", distance=" + distance; + if (vector != null) { + result += ", vector=" + Arrays.toString(vector); } + return result + "}"; + } } diff --git a/src/main/java/io/endee/client/types/SpaceType.java b/src/main/java/io/endee/client/types/SpaceType.java index fb96607..c3ed4cc 100644 --- a/src/main/java/io/endee/client/types/SpaceType.java +++ b/src/main/java/io/endee/client/types/SpaceType.java @@ -1,34 +1,32 @@ package io.endee.client.types; -/** - * Space types for distance calculation. - */ +/** Space types for distance calculation. */ public enum SpaceType { - COSINE("cosine"), - L2("l2"), - IP("ip"); + COSINE("cosine"), + L2("l2"), + IP("ip"); - private final String value; + private final String value; - SpaceType(String value) { - this.value = value; - } + SpaceType(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static SpaceType fromValue(String value) { - for (SpaceType t : values()) { - if (t.value.equalsIgnoreCase(value)) { - return t; - } - } - throw new IllegalArgumentException("Unknown space type: " + value); + public static SpaceType fromValue(String value) { + for (SpaceType t : values()) { + if (t.value.equalsIgnoreCase(value)) { + return t; + } } + throw new IllegalArgumentException("Unknown space type: " + value); + } - @Override - public String toString() { - return value; - } + @Override + public String toString() { + return value; + } } diff --git a/src/main/java/io/endee/client/types/VectorInfo.java b/src/main/java/io/endee/client/types/VectorInfo.java index 77b832b..358dc42 100644 --- a/src/main/java/io/endee/client/types/VectorInfo.java +++ b/src/main/java/io/endee/client/types/VectorInfo.java @@ -2,35 +2,64 @@ import java.util.Map; -/** - * Information about a vector retrieved from an index. - */ +/** Information about a vector retrieved from an index. */ public class VectorInfo { - private String id; - private Map meta; - private Map filter; - private double norm; - private double[] vector; + private String id; + private Map meta; + private Map filter; + private double norm; + private double[] vector; - public VectorInfo() {} + public VectorInfo() {} - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public Map getMeta() { return meta; } - public void setMeta(Map meta) { this.meta = meta; } + public void setId(String id) { + this.id = id; + } - public Map getFilter() { return filter; } - public void setFilter(Map filter) { this.filter = filter; } + public Map getMeta() { + return meta; + } - public double getNorm() { return norm; } - public void setNorm(double norm) { this.norm = norm; } + public void setMeta(Map meta) { + this.meta = meta; + } - public double[] getVector() { return vector; } - public void setVector(double[] vector) { this.vector = vector; } + public Map getFilter() { + return filter; + } - @Override - public String toString() { - return "VectorInfo{id='" + id + "', norm=" + norm + ", vectorLength=" + (vector != null ? vector.length : 0) + "}"; - } + public void setFilter(Map filter) { + this.filter = filter; + } + + public double getNorm() { + return norm; + } + + public void setNorm(double norm) { + this.norm = norm; + } + + public double[] getVector() { + return vector; + } + + public void setVector(double[] vector) { + this.vector = vector; + } + + @Override + public String toString() { + return "VectorInfo{id='" + + id + + "', norm=" + + norm + + ", vectorLength=" + + (vector != null ? vector.length : 0) + + "}"; + } } diff --git a/src/main/java/io/endee/client/types/VectorItem.java b/src/main/java/io/endee/client/types/VectorItem.java index f773405..e5a9734 100644 --- a/src/main/java/io/endee/client/types/VectorItem.java +++ b/src/main/java/io/endee/client/types/VectorItem.java @@ -2,62 +2,77 @@ import java.util.Map; -/** - * A vector item for upsert operations. - */ +/** A vector item for upsert operations. */ public class VectorItem { - private final String id; - private final double[] vector; - private Map meta; - private Map filter; - private int[] sparseIndices; - private double[] sparseValues; - - private VectorItem(String id, double[] vector) { - this.id = id; - this.vector = vector; + private final String id; + private final double[] vector; + private Map meta; + private Map filter; + private int[] sparseIndices; + private double[] sparseValues; + + private VectorItem(String id, double[] vector) { + this.id = id; + this.vector = vector; + } + + public static Builder builder(String id, double[] vector) { + return new Builder(id, vector); + } + + public String getId() { + return id; + } + + public double[] getVector() { + return vector; + } + + public Map getMeta() { + return meta; + } + + public Map getFilter() { + return filter; + } + + public int[] getSparseIndices() { + return sparseIndices; + } + + public double[] getSparseValues() { + return sparseValues; + } + + public static class Builder { + private final VectorItem item; + + private Builder(String id, double[] vector) { + this.item = new VectorItem(id, vector); + } + + public Builder meta(Map meta) { + item.meta = meta; + return this; + } + + public Builder filter(Map filter) { + item.filter = filter; + return this; + } + + public Builder sparseIndices(int[] sparseIndices) { + item.sparseIndices = sparseIndices; + return this; } - public static Builder builder(String id, double[] vector) { - return new Builder(id, vector); + public Builder sparseValues(double[] sparseValues) { + item.sparseValues = sparseValues; + return this; } - public String getId() { return id; } - public double[] getVector() { return vector; } - public Map getMeta() { return meta; } - public Map getFilter() { return filter; } - public int[] getSparseIndices() { return sparseIndices; } - public double[] getSparseValues() { return sparseValues; } - - public static class Builder { - private final VectorItem item; - - private Builder(String id, double[] vector) { - this.item = new VectorItem(id, vector); - } - - public Builder meta(Map meta) { - item.meta = meta; - return this; - } - - public Builder filter(Map filter) { - item.filter = filter; - return this; - } - - public Builder sparseIndices(int[] sparseIndices) { - item.sparseIndices = sparseIndices; - return this; - } - - public Builder sparseValues(double[] sparseValues) { - item.sparseValues = sparseValues; - return this; - } - - public VectorItem build() { - return item; - } + public VectorItem build() { + return item; } + } } diff --git a/src/main/java/io/endee/client/util/CryptoUtils.java b/src/main/java/io/endee/client/util/CryptoUtils.java index 832b402..da61436 100644 --- a/src/main/java/io/endee/client/util/CryptoUtils.java +++ b/src/main/java/io/endee/client/util/CryptoUtils.java @@ -1,10 +1,6 @@ package io.endee.client.util; import io.endee.client.exception.EndeeException; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; @@ -12,256 +8,250 @@ import java.util.Map; import java.util.zip.Deflater; import java.util.zip.Inflater; +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; /** - * Cryptographic utilities for Endee-DB. - * Provides compression/decompression and AES encryption/decryption for metadata. + * Cryptographic utilities for Endee-DB. Provides compression/decompression and AES + * encryption/decryption for metadata. */ public final class CryptoUtils { - private static final int AES_KEY_SIZE = 32; // 256 bits - private static final int IV_SIZE = 16; // 128 bits - private static final int BLOCK_SIZE = 16; - - private CryptoUtils() {} - - /** - * Gets checksum from key by converting last two hex characters to integer. - * - * @param key the key string - * @return checksum value or -1 if key is null - */ - public static int getChecksum(String key) { - if (key == null || key.length() < 2) { - return -1; - } - try { - String lastTwo = key.substring(key.length() - 2); - return Integer.parseInt(lastTwo, 16); - } catch (NumberFormatException e) { - return -1; - } + private static final int AES_KEY_SIZE = 32; // 256 bits + private static final int IV_SIZE = 16; // 128 bits + private static final int BLOCK_SIZE = 16; + + private CryptoUtils() {} + + /** + * Gets checksum from key by converting last two hex characters to integer. + * + * @param key the key string + * @return checksum value or -1 if key is null + */ + public static int getChecksum(String key) { + if (key == null || key.length() < 2) { + return -1; } - - /** - * Compresses a map to deflated JSON bytes, optionally encrypting with AES. - * - * @param data the map to compress - * @return compressed (and optionally encrypted) bytes - */ - public static byte[] jsonZip(Map data) { - if (data == null || data.isEmpty()) { - return new byte[0]; - } - try { - String json = JsonUtils.toJson(data); - byte[] compressed = deflateCompress(json.getBytes(StandardCharsets.UTF_8)); - - return compressed; - } catch (Exception e) { - throw new EndeeException("Failed to compress metadata", e); - } + try { + String lastTwo = key.substring(key.length() - 2); + return Integer.parseInt(lastTwo, 16); + } catch (NumberFormatException e) { + return -1; } - - /** - * Decompresses deflated JSON bytes to a map. - * - * @param data the compressed bytes - * @return the decompressed map - */ - public static Map jsonUnzip(byte[] data) { - return jsonUnzip(data, null); + } + + /** + * Compresses a map to deflated JSON bytes, optionally encrypting with AES. + * + * @param data the map to compress + * @return compressed (and optionally encrypted) bytes + */ + public static byte[] jsonZip(Map data) { + if (data == null || data.isEmpty()) { + return new byte[0]; } + try { + String json = JsonUtils.toJson(data); + byte[] compressed = deflateCompress(json.getBytes(StandardCharsets.UTF_8)); - /** - * Decompresses deflated JSON bytes to a map, optionally decrypting with AES. - * - * @param data the compressed (and optionally encrypted) bytes - * @param key optional hex key for AES decryption (64 hex chars = 256 bits) - * @return the decompressed map - */ - @SuppressWarnings("unchecked") - public static Map jsonUnzip(byte[] data, String key) { - if (data == null || data.length == 0) { - return Map.of(); - } - try { - byte[] buffer = data; - - // If key is provided, decrypt first - if (key != null && !key.isEmpty()) { - buffer = aesDecrypt(buffer, key); - } - - byte[] decompressed = deflateDecompress(buffer); - String json = new String(decompressed, StandardCharsets.UTF_8); - return JsonUtils.fromJson(json, Map.class); - } catch (Exception e) { - // Return empty map on failure (matches TypeScript behavior) - return Map.of(); - } + return compressed; + } catch (Exception e) { + throw new EndeeException("Failed to compress metadata", e); } - - /** - * Encrypts data using AES-256-CBC. - * - * @param data the data to encrypt - * @param keyHex a 256-bit hex key (64 hex characters) - * @return IV + ciphertext (IV is prepended to the ciphertext) - */ - public static byte[] aesEncrypt(byte[] data, String keyHex) { - try { - byte[] key = hexToBytes(keyHex); - - if (key.length != AES_KEY_SIZE) { - throw new IllegalArgumentException("Key must be 256 bits (64 hex characters)"); - } - - // Generate random IV - byte[] iv = new byte[IV_SIZE]; - new SecureRandom().nextBytes(iv); - - // Pad data with PKCS7 - byte[] paddedData = pkcs7Pad(data); - - // Create cipher and encrypt - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - - byte[] ciphertext = cipher.doFinal(paddedData); - - // Return IV + ciphertext - byte[] result = new byte[iv.length + ciphertext.length]; - System.arraycopy(iv, 0, result, 0, iv.length); - System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length); - - return result; - } catch (IllegalArgumentException e) { - throw e; - } catch (Exception e) { - throw new EndeeException("AES encryption failed", e); - } + } + + /** + * Decompresses deflated JSON bytes to a map. + * + * @param data the compressed bytes + * @return the decompressed map + */ + public static Map jsonUnzip(byte[] data) { + return jsonUnzip(data, null); + } + + /** + * Decompresses deflated JSON bytes to a map, optionally decrypting with AES. + * + * @param data the compressed (and optionally encrypted) bytes + * @param key optional hex key for AES decryption (64 hex chars = 256 bits) + * @return the decompressed map + */ + @SuppressWarnings("unchecked") + public static Map jsonUnzip(byte[] data, String key) { + if (data == null || data.length == 0) { + return Map.of(); } - - /** - * Decrypts data using AES-256-CBC. - * - * @param data the encrypted data (IV + ciphertext) - * @param keyHex a 256-bit hex key (64 hex characters) - * @return the decrypted data - */ - public static byte[] aesDecrypt(byte[] data, String keyHex) { - try { - byte[] key = hexToBytes(keyHex); - - if (key.length != AES_KEY_SIZE) { - throw new IllegalArgumentException("Key must be 256 bits (64 hex characters)"); - } - - if (data.length < IV_SIZE) { - throw new IllegalArgumentException("Data too short to contain IV"); - } - - // Extract IV and ciphertext - byte[] iv = Arrays.copyOfRange(data, 0, IV_SIZE); - byte[] ciphertext = Arrays.copyOfRange(data, IV_SIZE, data.length); - - // Create cipher and decrypt - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); - - byte[] paddedData = cipher.doFinal(ciphertext); - - // Remove PKCS7 padding - return pkcs7Unpad(paddedData); - } catch (IllegalArgumentException e) { - throw e; - } catch (Exception e) { - throw new EndeeException("AES decryption failed", e); - } + try { + byte[] buffer = data; + + // If key is provided, decrypt first + if (key != null && !key.isEmpty()) { + buffer = aesDecrypt(buffer, key); + } + + byte[] decompressed = deflateDecompress(buffer); + String json = new String(decompressed, StandardCharsets.UTF_8); + return JsonUtils.fromJson(json, Map.class); + } catch (Exception e) { + // Return empty map on failure (matches TypeScript behavior) + return Map.of(); } + } + + /** + * Encrypts data using AES-256-CBC. + * + * @param data the data to encrypt + * @param keyHex a 256-bit hex key (64 hex characters) + * @return IV + ciphertext (IV is prepended to the ciphertext) + */ + public static byte[] aesEncrypt(byte[] data, String keyHex) { + try { + byte[] key = hexToBytes(keyHex); + + if (key.length != AES_KEY_SIZE) { + throw new IllegalArgumentException("Key must be 256 bits (64 hex characters)"); + } + + // Generate random IV + byte[] iv = new byte[IV_SIZE]; + new SecureRandom().nextBytes(iv); + + // Pad data with PKCS7 + byte[] paddedData = pkcs7Pad(data); + + // Create cipher and encrypt + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + + byte[] ciphertext = cipher.doFinal(paddedData); + + // Return IV + ciphertext + byte[] result = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length); + + return result; + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new EndeeException("AES encryption failed", e); + } + } + + /** + * Decrypts data using AES-256-CBC. + * + * @param data the encrypted data (IV + ciphertext) + * @param keyHex a 256-bit hex key (64 hex characters) + * @return the decrypted data + */ + public static byte[] aesDecrypt(byte[] data, String keyHex) { + try { + byte[] key = hexToBytes(keyHex); + + if (key.length != AES_KEY_SIZE) { + throw new IllegalArgumentException("Key must be 256 bits (64 hex characters)"); + } + + if (data.length < IV_SIZE) { + throw new IllegalArgumentException("Data too short to contain IV"); + } + + // Extract IV and ciphertext + byte[] iv = Arrays.copyOfRange(data, 0, IV_SIZE); + byte[] ciphertext = Arrays.copyOfRange(data, IV_SIZE, data.length); + + // Create cipher and decrypt + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + byte[] paddedData = cipher.doFinal(ciphertext); + + // Remove PKCS7 padding + return pkcs7Unpad(paddedData); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new EndeeException("AES decryption failed", e); + } + } - /** - * Compresses data using DEFLATE algorithm (raw, no headers). - */ - private static byte[] deflateCompress(byte[] data) { - Deflater deflater = new Deflater(); - deflater.setInput(data); - deflater.finish(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); - byte[] buffer = new byte[1024]; + /** Compresses data using DEFLATE algorithm (raw, no headers). */ + private static byte[] deflateCompress(byte[] data) { + Deflater deflater = new Deflater(); + deflater.setInput(data); + deflater.finish(); - while (!deflater.finished()) { - int count = deflater.deflate(buffer); - bos.write(buffer, 0, count); - } + ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); + byte[] buffer = new byte[1024]; - deflater.end(); - return bos.toByteArray(); + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + bos.write(buffer, 0, count); } - /** - * Decompresses DEFLATE compressed data. - */ - private static byte[] deflateDecompress(byte[] data) throws Exception { - Inflater inflater = new Inflater(); - inflater.setInput(data); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); - byte[] buffer = new byte[1024]; - - while (!inflater.finished()) { - int count = inflater.inflate(buffer); - if (count == 0 && inflater.needsInput()) { - break; - } - bos.write(buffer, 0, count); - } - - inflater.end(); - return bos.toByteArray(); - } + deflater.end(); + return bos.toByteArray(); + } - /** - * Adds PKCS7 padding to data. - */ - private static byte[] pkcs7Pad(byte[] data) { - int paddingLength = BLOCK_SIZE - (data.length % BLOCK_SIZE); - byte[] padded = new byte[data.length + paddingLength]; - System.arraycopy(data, 0, padded, 0, data.length); - Arrays.fill(padded, data.length, padded.length, (byte) paddingLength); - return padded; - } + /** Decompresses DEFLATE compressed data. */ + private static byte[] deflateDecompress(byte[] data) throws Exception { + Inflater inflater = new Inflater(); + inflater.setInput(data); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); + byte[] buffer = new byte[1024]; - /** - * Removes PKCS7 padding from data. - */ - private static byte[] pkcs7Unpad(byte[] data) { - if (data.length == 0) { - return data; - } - int paddingLength = data[data.length - 1] & 0xFF; - if (paddingLength > BLOCK_SIZE || paddingLength > data.length) { - return data; // Invalid padding, return as-is - } - return Arrays.copyOfRange(data, 0, data.length - paddingLength); + while (!inflater.finished()) { + int count = inflater.inflate(buffer); + if (count == 0 && inflater.needsInput()) { + break; + } + bos.write(buffer, 0, count); } - /** - * Converts a hex string to byte array. - */ - private static byte[] hexToBytes(String hex) { - int len = hex.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) - + Character.digit(hex.charAt(i + 1), 16)); - } - return data; + inflater.end(); + return bos.toByteArray(); + } + + /** Adds PKCS7 padding to data. */ + private static byte[] pkcs7Pad(byte[] data) { + int paddingLength = BLOCK_SIZE - (data.length % BLOCK_SIZE); + byte[] padded = new byte[data.length + paddingLength]; + System.arraycopy(data, 0, padded, 0, data.length); + Arrays.fill(padded, data.length, padded.length, (byte) paddingLength); + return padded; + } + + /** Removes PKCS7 padding from data. */ + private static byte[] pkcs7Unpad(byte[] data) { + if (data.length == 0) { + return data; + } + int paddingLength = data[data.length - 1] & 0xFF; + if (paddingLength > BLOCK_SIZE || paddingLength > data.length) { + return data; // Invalid padding, return as-is + } + return Arrays.copyOfRange(data, 0, data.length - paddingLength); + } + + /** Converts a hex string to byte array. */ + private static byte[] hexToBytes(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = + (byte) + ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); } + return data; + } } diff --git a/src/main/java/io/endee/client/util/JsonUtils.java b/src/main/java/io/endee/client/util/JsonUtils.java index 4d5aa2a..a1f2775 100644 --- a/src/main/java/io/endee/client/util/JsonUtils.java +++ b/src/main/java/io/endee/client/util/JsonUtils.java @@ -5,33 +5,31 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.endee.client.exception.EndeeException; -/** - * JSON serialization utilities. - */ +/** JSON serialization utilities. */ public final class JsonUtils { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final ObjectMapper OBJECT_MAPPER = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - private JsonUtils() {} + private JsonUtils() {} - public static ObjectMapper getObjectMapper() { - return OBJECT_MAPPER; - } + public static ObjectMapper getObjectMapper() { + return OBJECT_MAPPER; + } - public static String toJson(Object object) { - try { - return OBJECT_MAPPER.writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new EndeeException("Failed to serialize to JSON", e); - } + public static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new EndeeException("Failed to serialize to JSON", e); } + } - public static T fromJson(String json, Class type) { - try { - return OBJECT_MAPPER.readValue(json, type); - } catch (JsonProcessingException e) { - throw new EndeeException("Failed to deserialize JSON", e); - } + public static T fromJson(String json, Class type) { + try { + return OBJECT_MAPPER.readValue(json, type); + } catch (JsonProcessingException e) { + throw new EndeeException("Failed to deserialize JSON", e); } + } } diff --git a/src/main/java/io/endee/client/util/MessagePackUtils.java b/src/main/java/io/endee/client/util/MessagePackUtils.java index bb1faed..b6ab743 100644 --- a/src/main/java/io/endee/client/util/MessagePackUtils.java +++ b/src/main/java/io/endee/client/util/MessagePackUtils.java @@ -1,164 +1,155 @@ package io.endee.client.util; import io.endee.client.exception.EndeeException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.msgpack.core.MessageBufferPacker; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; import org.msgpack.value.Value; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * MessagePack serialization utilities. - */ +/** MessagePack serialization utilities. */ public final class MessagePackUtils { - private MessagePackUtils() { - } + private MessagePackUtils() {} - /** - * Packs vector data for upsert operations. - */ - public static byte[] packVectors(List vectors) { - try (MessageBufferPacker packer = MessagePack.newDefaultBufferPacker()) { - packer.packArrayHeader(vectors.size()); + /** Packs vector data for upsert operations. */ + public static byte[] packVectors(List vectors) { + try (MessageBufferPacker packer = MessagePack.newDefaultBufferPacker()) { + packer.packArrayHeader(vectors.size()); - for (Object[] vector : vectors) { - packVectorTuple(packer, vector); - } + for (Object[] vector : vectors) { + packVectorTuple(packer, vector); + } - return packer.toByteArray(); - } catch (IOException e) { - throw new EndeeException("Failed to pack vectors", e); - } + return packer.toByteArray(); + } catch (IOException e) { + throw new EndeeException("Failed to pack vectors", e); } + } - private static void packVectorTuple(MessageBufferPacker packer, Object[] vector) throws IOException { - packer.packArrayHeader(vector.length); - - // id (string) - packer.packString((String) vector[0]); + private static void packVectorTuple(MessageBufferPacker packer, Object[] vector) + throws IOException { + packer.packArrayHeader(vector.length); - // metadata (bytes) - byte[] meta = (byte[]) vector[1]; - packer.packBinaryHeader(meta.length); - packer.writePayload(meta); + // id (string) + packer.packString((String) vector[0]); - // filter (string) - packer.packString((String) vector[2]); + // metadata (bytes) + byte[] meta = (byte[]) vector[1]; + packer.packBinaryHeader(meta.length); + packer.writePayload(meta); - // norm (double) - packer.packDouble((Double) vector[3]); + // filter (string) + packer.packString((String) vector[2]); - // vector (double[]) - double[] vec = (double[]) vector[4]; - packer.packArrayHeader(vec.length); - for (double v : vec) { - packer.packDouble(v); - } + // norm (double) + packer.packDouble((Double) vector[3]); - // Optional sparse data - if (vector.length > 5) { - int[] sparseIndices = (int[]) vector[5]; - packer.packArrayHeader(sparseIndices.length); - for (int idx : sparseIndices) { - packer.packInt(idx); - } - - double[] sparseValues = (double[]) vector[6]; - packer.packArrayHeader(sparseValues.length); - for (double val : sparseValues) { - packer.packDouble(val); - } - } + // vector (double[]) + double[] vec = (double[]) vector[4]; + packer.packArrayHeader(vec.length); + for (double v : vec) { + packer.packDouble(v); } - /** - * Unpacks query results from MessagePack bytes. - */ - public static List unpackQueryResults(byte[] data) { - List results = new ArrayList<>(); - - try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data)) { - int arraySize = unpacker.unpackArrayHeader(); - - for (int i = 0; i < arraySize; i++) { - int tupleSize = unpacker.unpackArrayHeader(); - Object[] tuple = new Object[tupleSize]; - - tuple[0] = unpackNumberAsDouble(unpacker); // similarity - tuple[1] = unpacker.unpackString(); // vectorId - int metaLen = unpacker.unpackBinaryHeader(); - tuple[2] = unpacker.readPayload(metaLen); // metadata - tuple[3] = unpacker.unpackString(); // filter - tuple[4] = unpackNumberAsDouble(unpacker); // norm - - if (tupleSize > 5) { - int vecLen = unpacker.unpackArrayHeader(); - double[] vec = new double[vecLen]; - for (int j = 0; j < vecLen; j++) { - vec[j] = unpackNumberAsDouble(unpacker); - } - tuple[5] = vec; - } - - results.add(tuple); - } - } catch (IOException e) { - throw new EndeeException("Failed to unpack query results", e); + // Optional sparse data + if (vector.length > 5) { + int[] sparseIndices = (int[]) vector[5]; + packer.packArrayHeader(sparseIndices.length); + for (int idx : sparseIndices) { + packer.packInt(idx); + } + + double[] sparseValues = (double[]) vector[6]; + packer.packArrayHeader(sparseValues.length); + for (double val : sparseValues) { + packer.packDouble(val); + } + } + } + + /** Unpacks query results from MessagePack bytes. */ + public static List unpackQueryResults(byte[] data) { + List results = new ArrayList<>(); + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data)) { + int arraySize = unpacker.unpackArrayHeader(); + + for (int i = 0; i < arraySize; i++) { + int tupleSize = unpacker.unpackArrayHeader(); + Object[] tuple = new Object[tupleSize]; + + tuple[0] = unpackNumberAsDouble(unpacker); // similarity + tuple[1] = unpacker.unpackString(); // vectorId + int metaLen = unpacker.unpackBinaryHeader(); + tuple[2] = unpacker.readPayload(metaLen); // metadata + tuple[3] = unpacker.unpackString(); // filter + tuple[4] = unpackNumberAsDouble(unpacker); // norm + + if (tupleSize > 5) { + int vecLen = unpacker.unpackArrayHeader(); + double[] vec = new double[vecLen]; + for (int j = 0; j < vecLen; j++) { + vec[j] = unpackNumberAsDouble(unpacker); + } + tuple[5] = vec; } - return results; + results.add(tuple); + } + } catch (IOException e) { + throw new EndeeException("Failed to unpack query results", e); } - /** - * Unpacks a single vector from MessagePack bytes. - */ - public static Object[] unpackVector(byte[] data) { - try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data)) { - int tupleSize = unpacker.unpackArrayHeader(); - Object[] tuple = new Object[tupleSize]; - - tuple[0] = unpacker.unpackString(); // id - int metaLen = unpacker.unpackBinaryHeader(); - tuple[1] = unpacker.readPayload(metaLen); // metadata - tuple[2] = unpacker.unpackString(); // filter - tuple[3] = unpackNumberAsDouble(unpacker); // norm - - int vecLen = unpacker.unpackArrayHeader(); - double[] vec = new double[vecLen]; - for (int i = 0; i < vecLen; i++) { - vec[i] = unpackNumberAsDouble(unpacker); - } - tuple[4] = vec; - - return tuple; - } catch (IOException e) { - throw new EndeeException("Failed to unpack vector", e); - } + return results; + } + + /** Unpacks a single vector from MessagePack bytes. */ + public static Object[] unpackVector(byte[] data) { + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data)) { + int tupleSize = unpacker.unpackArrayHeader(); + Object[] tuple = new Object[tupleSize]; + + tuple[0] = unpacker.unpackString(); // id + int metaLen = unpacker.unpackBinaryHeader(); + tuple[1] = unpacker.readPayload(metaLen); // metadata + tuple[2] = unpacker.unpackString(); // filter + tuple[3] = unpackNumberAsDouble(unpacker); // norm + + int vecLen = unpacker.unpackArrayHeader(); + double[] vec = new double[vecLen]; + for (int i = 0; i < vecLen; i++) { + vec[i] = unpackNumberAsDouble(unpacker); + } + tuple[4] = vec; + + return tuple; + } catch (IOException e) { + throw new EndeeException("Failed to unpack vector", e); + } + } + + /** + * Helper function to unpackNumber as double even though it can be integer + * + * @param unpacker + * @return + * @throws IOException + */ + private static double unpackNumberAsDouble(MessageUnpacker unpacker) throws IOException { + Value value = unpacker.unpackValue(); + + if (value.isFloatValue()) { + return value.asFloatValue().toDouble(); } - /** - * Helper function to unpackNumber as double even though it - * can be integer - * @param unpacker - * @return - * @throws IOException - */ - private static double unpackNumberAsDouble(MessageUnpacker unpacker) throws IOException { - Value value = unpacker.unpackValue(); - - if (value.isFloatValue()) { - return value.asFloatValue().toDouble(); - } - - if (value.isIntegerValue()) { - return value.asIntegerValue().toDouble(); - } - - throw new IllegalStateException( - "Expected numeric value (int/float), got " + value.getValueType()); + if (value.isIntegerValue()) { + return value.asIntegerValue().toDouble(); } + + throw new IllegalStateException( + "Expected numeric value (int/float), got " + value.getValueType()); + } } diff --git a/src/main/java/io/endee/client/util/ValidationUtils.java b/src/main/java/io/endee/client/util/ValidationUtils.java index 17afc16..b617d04 100644 --- a/src/main/java/io/endee/client/util/ValidationUtils.java +++ b/src/main/java/io/endee/client/util/ValidationUtils.java @@ -5,50 +5,43 @@ import java.util.Set; import java.util.regex.Pattern; -/** - * Validation utilities. - */ +/** Validation utilities. */ public final class ValidationUtils { - private static final Pattern INDEX_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$"); - private static final int MAX_INDEX_NAME_LENGTH = 48; - - private ValidationUtils() {} - - /** - * Validates an index name. - * Must be alphanumeric with underscores, less than 48 characters. - */ - public static boolean isValidIndexName(String name) { - if (name == null || name.isEmpty()) { - return false; - } - if (name.length() >= MAX_INDEX_NAME_LENGTH) { - return false; - } - return INDEX_NAME_PATTERN.matcher(name).matches(); + private static final Pattern INDEX_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$"); + private static final int MAX_INDEX_NAME_LENGTH = 48; + + private ValidationUtils() {} + + /** Validates an index name. Must be alphanumeric with underscores, less than 48 characters. */ + public static boolean isValidIndexName(String name) { + if (name == null || name.isEmpty()) { + return false; + } + if (name.length() >= MAX_INDEX_NAME_LENGTH) { + return false; + } + return INDEX_NAME_PATTERN.matcher(name).matches(); + } + + /** Validates that all vector IDs are non-empty and unique. */ + public static void validateVectorIds(List ids) { + Set seenIds = new HashSet<>(); + Set duplicateIds = new HashSet<>(); + + for (String id : ids) { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("All vectors must have a non-empty ID"); + } + if (seenIds.contains(id)) { + duplicateIds.add(id); + } else { + seenIds.add(id); + } } - /** - * Validates that all vector IDs are non-empty and unique. - */ - public static void validateVectorIds(List ids) { - Set seenIds = new HashSet<>(); - Set duplicateIds = new HashSet<>(); - - for (String id : ids) { - if (id == null || id.isEmpty()) { - throw new IllegalArgumentException("All vectors must have a non-empty ID"); - } - if (seenIds.contains(id)) { - duplicateIds.add(id); - } else { - seenIds.add(id); - } - } - - if (!duplicateIds.isEmpty()) { - throw new IllegalArgumentException("Duplicate IDs found: " + String.join(", ", duplicateIds)); - } + if (!duplicateIds.isEmpty()) { + throw new IllegalArgumentException("Duplicate IDs found: " + String.join(", ", duplicateIds)); } + } }