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);
+ }
+
+ // ==================== 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);
+ }
+
+ 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);
+ }
+
+ 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();
+
+ 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));
+
+ 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
new file mode 100644
index 0000000..1a6ab68
--- /dev/null
+++ b/src/main/java/io/endee/client/exception/EndeeApiException.java
@@ -0,0 +1,40 @@
+package io.endee.client.exception;
+
+/**
+ * Exception thrown when the Endee API returns an error response.
+ */
+public class EndeeApiException extends EndeeException {
+
+ 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 int getStatusCode() {
+ return statusCode;
+ }
+
+ 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;
+ };
+ 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
new file mode 100644
index 0000000..ea56c96
--- /dev/null
+++ b/src/main/java/io/endee/client/exception/EndeeException.java
@@ -0,0 +1,19 @@
+package io.endee.client.exception;
+
+/**
+ * Base exception for all Endee client errors.
+ */
+public class EndeeException extends RuntimeException {
+
+ public EndeeException(String message) {
+ super(message);
+ }
+
+ public EndeeException(String message, Throwable cause) {
+ super(message, 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
new file mode 100644
index 0000000..30ac60c
--- /dev/null
+++ b/src/main/java/io/endee/client/types/CreateIndexOptions.java
@@ -0,0 +1,75 @@
+package io.endee.client.types;
+
+/**
+ * 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.INT8D;
+ 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 Builder sparseDimension(Integer sparseDimension) {
+ options.sparseDimension = sparseDimension;
+ return this;
+ }
+
+ 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
new file mode 100644
index 0000000..045b96d
--- /dev/null
+++ b/src/main/java/io/endee/client/types/IndexDescription.java
@@ -0,0 +1,43 @@
+package io.endee.client.types;
+
+/**
+ * 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 +"}";
+ }
+}
diff --git a/src/main/java/io/endee/client/types/IndexInfo.java b/src/main/java/io/endee/client/types/IndexInfo.java
new file mode 100644
index 0000000..f732c44
--- /dev/null
+++ b/src/main/java/io/endee/client/types/IndexInfo.java
@@ -0,0 +1,45 @@
+package io.endee.client.types;
+
+/**
+ * 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;
+
+ public IndexInfo() {}
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+
+ public SpaceType getSpaceType() { return spaceType; }
+ public void setSpaceType(SpaceType spaceType) { this.spaceType = spaceType; }
+
+ public int getDimension() { return dimension; }
+ public void setDimension(int dimension) { this.dimension = dimension; }
+
+ public long getTotalElements() { return totalElements; }
+ public void setTotalElements(long totalElements) { this.totalElements = totalElements; }
+
+ 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 Integer getVersion() { return version; }
+ public void setVersion(Integer version) { this.version = version; }
+
+ public Integer getSparseDimension() { return sparseDimension; }
+ public void setSparseDimension(Integer sparseDimension) { this.sparseDimension = sparseDimension; }
+}
diff --git a/src/main/java/io/endee/client/types/Precision.java b/src/main/java/io/endee/client/types/Precision.java
new file mode 100644
index 0000000..1f0569f
--- /dev/null
+++ b/src/main/java/io/endee/client/types/Precision.java
@@ -0,0 +1,36 @@
+package io.endee.client.types;
+
+/**
+ * Precision types for vector quantization.
+ */
+public enum Precision {
+ BINARY("binary"),
+ INT8D("int8d"),
+ INT16D("int16d"),
+ FLOAT32("float32"),
+ FLOAT16("float16");
+
+ private final String value;
+
+ Precision(String value) {
+ this.value = 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);
+ }
+
+ @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
new file mode 100644
index 0000000..002d70d
--- /dev/null
+++ b/src/main/java/io/endee/client/types/QueryOptions.java
@@ -0,0 +1,93 @@
+package io.endee.client.types;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Options for querying an Endee index.
+ *
+ * Example usage with filters:
+ * {@code
+ * QueryOptions options = QueryOptions.builder()
+ * .vector(new double[]{0.1, 0.2, 0.3})
+ * .topK(10)
+ * .filter(List.of(
+ * Map.of("category", Map.of("$eq", "tech")),
+ * Map.of("score", Map.of("$range", List.of(80, 100)))
+ * ))
+ * .build();
+ * }
+ */
+public class QueryOptions {
+ private double[] vector;
+ private int topK;
+ private List