From d7b8c843fa3b34343c70d9cbaa4b5ee1195fda89 Mon Sep 17 00:00:00 2001 From: Pankaj Singh Date: Thu, 13 Aug 2026 16:03:11 +0530 Subject: [PATCH 1/2] feat: add getNeighborsById() method --- README.md | 98 ++++++++++--------- src/main/java/io/endee/client/Collection.java | 42 +++++++- src/main/java/io/endee/client/Endee.java | 52 +++++----- 3 files changed, 115 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 38df226..0d22e13 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ Map result = client.createCollection("my_docs", List.of( "sparse_model", "default" // "default" or "endee_bm25" ) )); -// Output: {message=collection created} ``` **Field types:** @@ -101,18 +100,15 @@ Map result = client.createCollection("my_docs", List.of( ```java // List all collections List> collections = client.listCollections(); -// Output: [{name=my_docs, fields=[...], count=1000}, ...] // Get a collection reference (for upsert, search, etc.) Collection collection = client.getCollection("my_docs"); // Describe a collection (refreshes metadata from server) Map desc = collection.describe(); -// Output: {name=my_docs, fields=[{name=embedding, type=vector, params={...}}, ...], count=1000} // Delete a collection (irreversible) client.deleteCollection("my_docs"); -// Output: {message=collection deleted} ``` --- @@ -147,7 +143,6 @@ List objects = List.of( ); Map result = collection.upsert(objects); -// Output: {message=2 objects upserted} ``` **ObjectItem fields:** @@ -186,9 +181,6 @@ for (SearchHit hit : results.get("embedding")) { System.out.printf("ID: %s Score: %.4f Meta: %s Filter: %s%n", hit.getId(), hit.getSimilarity(), hit.getMeta(), hit.getFilter()); } -// Output: -// ID: doc1 Score: 0.9823 Meta: {title=First Document, author=Alice} Filter: {category=tech, year=2024} -// ID: doc2 Score: 0.9156 Meta: {title=Second Document, author=Bob} Filter: {category=science, year=2023} ``` ### Filtered Search @@ -258,10 +250,6 @@ List fused = Reranker.rerank( for (SearchHit hit : fused) { System.out.printf("ID: %s RRF Score: %.6f%n", hit.getId(), hit.getSimilarity()); } -// Output: -// ID: doc1 RRF Score: 0.016393 -// ID: doc2 RRF Score: 0.013115 -// ... // Convenience: uniform weights, default limit (10) and k (60) List fused = Reranker.rerank(results, Map.of("embedding", 0.5, "keywords", 0.5)); @@ -318,18 +306,27 @@ for (ObjectInfo obj : objects) { --- +## Get Neighbors + +Get the HNSW graph neighbors of an object for a given field: + +```java +Map neighbors = collection.getNeighborsById("doc1", "embedding"); +System.out.println(neighbors); +``` + +--- + ## Delete Objects ```java // Delete by ID -Map result = collection.deleteObject("doc1"); -// Output: {message=1 rows deleted} +collection.deleteObject("doc1"); // Delete by filter -Map result = collection.deleteByFilter( +collection.deleteByFilter( List.of(Map.of("category", Map.of("$eq", "tech"))) ); -// Output: {message=5 rows deleted} ``` --- @@ -341,11 +338,10 @@ Update filter fields on existing objects without re-upserting. The entire filter ```java import io.endee.client.types.UpdateFilterParams; -Map result = collection.updateFilters(List.of( +collection.updateFilters(List.of( new UpdateFilterParams("doc1", Map.of("category", "ml", "year", 2025)), new UpdateFilterParams("doc2", Map.of("category", "physics", "year", 2024)) )); -// Output: {message=2 filters updated} ``` --- @@ -357,17 +353,14 @@ Map result = collection.updateFilters(List.of( Rebuilds HNSW graphs with new parameters. Runs asynchronously — poll `rebuildStatus()` until complete: ```java -// Trigger rebuild -Map result = collection.rebuild( +collection.rebuild( List.of(Map.of("field", "embedding", "M", 20, "ef_con", 200)) ); -// Output: {message=rebuild started} // Poll until complete while (true) { Map status = collection.rebuildStatus(); System.out.println(status); - // Output: {status=in_progress, vectors_processed=500, total_vectors=1000, percent_complete=50} if ("completed".equals(status.get("status"))) break; Thread.sleep(2000); } @@ -378,8 +371,7 @@ while (true) { Defragments the collection's storage after deletions: ```java -Map result = collection.shrink(); -// Output: {message=shrink complete} +collection.shrink(); ``` --- @@ -389,14 +381,13 @@ Map result = collection.shrink(); ### Collection-level Backup ```java -// Create a backup (async — poll activeBackup() until done) -Map result = collection.createBackup("my_backup"); -// Output: {message=backup started} +// Create a backup (async — poll backupStatus() until done) +collection.createBackup("my_backup"); // Poll until complete while (true) { - Map active = client.activeBackup(); - if (!Boolean.TRUE.equals(active.get("active"))) break; + Map status = client.backupStatus(); + if ("completed".equals(status.get("status")) || "idle".equals(status.get("status"))) break; Thread.sleep(2000); } ``` @@ -410,11 +401,18 @@ Object backups = client.listBackups(); // Get backup info Map info = client.backupInfo("my_backup"); -// Active backup status -Map active = client.activeBackup(); +// Backup status +Map status = client.backupStatus(); // Restore a backup into a new collection -Map result = client.restoreBackup("my_backup", "restored_collection"); +client.restoreBackup("my_backup", "restored_collection"); + +// Poll restore status until complete +while (true) { + Map restoreStatus = client.restoreStatus(); + if ("completed".equals(restoreStatus.get("status")) || "idle".equals(restoreStatus.get("status"))) break; + Thread.sleep(2000); +} // Delete a backup client.deleteBackup("my_backup"); @@ -424,15 +422,16 @@ client.deleteBackup("my_backup"); ```java // Download a backup as a .tar file -String path = client.downloadBackup("my_backup", "/tmp/my_backup.tar"); -// Output: "/tmp/my_backup.tar" +client.downloadBackup("my_backup", "/tmp/my_backup.tar"); // Download with db_name (for root-token multi-database targeting) client.downloadBackup("my_backup", "/tmp/my_backup.tar", "my_database"); // Upload a .tar backup file -Map result = client.uploadBackup("/tmp/my_backup.tar"); -// Output: {message=backup uploaded} +client.uploadBackup("/tmp/my_backup.tar"); + +// Upload with a custom backup name +client.uploadBackup("/tmp/my_backup.tar", "custom_name"); ``` --- @@ -442,11 +441,9 @@ Map result = client.uploadBackup("/tmp/my_backup.tar"); ```java // Health check Map health = client.health(); -// Output: {status=ok, timestamp=1234567890} // Server stats Map stats = client.stats(); -// Output: {version=2.0.0, uptime=3600, total_requests=15000} ``` --- @@ -461,8 +458,8 @@ Admin operations require a root token. Endee admin = new Endee("root_token"); // Create a database (returns the new db token) -String dbToken = admin.createDatabase("my_db", "enterprise"); // db_type options: "starter", "pro", "scale", "enterprise" +String dbToken = admin.createDatabase("my_db", "enterprise"); // List all databases List> dbs = admin.listDatabases(); @@ -498,8 +495,8 @@ admin.deleteDbCollection("my_db", "my_collection"); ```java // Create a token for a database -String token = admin.createToken("my_db", "analytics_token", "r"); // token_type: "rw" (read-write) or "r" (read-only) +String token = admin.createToken("my_db", "analytics_token", "r"); // List tokens List> tokens = admin.listTokens("my_db"); @@ -650,28 +647,32 @@ public class Example { // 6. Get full objects List objects = collection.getObjects(List.of("doc1")); - System.out.println("Vectors: " + objects.get(0).getVectors().keySet()); - // 7. Update filters + // 7. Get neighbors + Map neighbors = collection.getNeighborsById("doc1", "embedding"); + + // 8. Update filters collection.updateFilters(List.of( new UpdateFilterParams("doc1", Map.of("category", "ml", "score", 95)) )); - // 8. Rebuild and wait + // 9. Rebuild and wait collection.rebuild(List.of(Map.of("field", "embedding", "M", 20, "ef_con", 200))); while (!"completed".equals(collection.rebuildStatus().get("status"))) { Thread.sleep(2000); } - // 9. Backup, download, restore + // 10. Backup, download, restore collection.createBackup("my_backup"); - while (Boolean.TRUE.equals(client.activeBackup().get("active"))) { + while (true) { + Map status = client.backupStatus(); + if ("completed".equals(status.get("status")) || "idle".equals(status.get("status"))) break; Thread.sleep(2000); } client.downloadBackup("my_backup", "/tmp/my_backup.tar"); client.restoreBackup("my_backup", "docs_restored"); - // 10. Cleanup + // 11. Cleanup client.deleteCollection("docs"); client.deleteCollection("docs_restored"); client.deleteBackup("my_backup"); @@ -699,12 +700,14 @@ public class Example { | `stats()` | `Map` | Server stats | | `listBackups()` | `Object` | List backups | | `backupInfo(name)` | `Map` | Get backup metadata | -| `activeBackup()` | `Map` | Get active backup status | +| `backupStatus()` | `Map` | Get backup status | +| `restoreStatus()` | `Map` | Get restore status | | `restoreBackup(name, target)` | `Map` | Restore backup to new collection | | `deleteBackup(name)` | `Map` | Delete a backup | | `downloadBackup(name, destPath)` | `String` | Download backup as .tar | | `downloadBackup(name, destPath, dbName)` | `String` | Download backup (multi-db) | | `uploadBackup(filePath)` | `Map` | Upload a .tar backup | +| `uploadBackup(filePath, backupName)` | `Map` | Upload a .tar backup with custom name | | `createDatabase(name, type)` | `String` | Create database (admin) | | `listDatabases()` | `List` | List databases (admin) | | `getDatabase(name)` | `Map` | Get database info (admin) | @@ -731,6 +734,7 @@ public class Example { | `search(queryFields, filter)` | `Map>` | Search with filter | | `search(queryFields, filter, efSearch, prefilterThreshold, boostPct)` | `Map>` | Search with all options | | `getObjects(List ids)` | `List` | Fetch full objects by ID | +| `getNeighborsById(id, field)` | `Map` | Get HNSW graph neighbors | | `deleteObject(String id)` | `Map` | Delete object by ID | | `deleteByFilter(List)` | `Map` | Delete objects matching filter | | `updateFilters(List)` | `Map` | Update filter fields | diff --git a/src/main/java/io/endee/client/Collection.java b/src/main/java/io/endee/client/Collection.java index a9aef00..c0e3627 100644 --- a/src/main/java/io/endee/client/Collection.java +++ b/src/main/java/io/endee/client/Collection.java @@ -491,6 +491,44 @@ public List getObjects(List ids) { } } + // ── get neighbors ──────────────────────────────────────────────────────────── + + /** + * Gets the HNSW graph neighbors of an object for a given field. + * + * @param id object ID + * @param field field name + * @return map with id, field, and links + */ + public Map getNeighborsById(String id, String field) { + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("id is required"); + } + if (field == null || field.isEmpty()) { + throw new IllegalArgumentException("field is required"); + } + + try { + HttpRequest request = + buildGetRequest("/collection/" + name + "/objects/" + id + "/field/" + field + "/links"); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + EndeeApiException.raiseException(response.statusCode(), response.body()); + } + + @SuppressWarnings("unchecked") + Map result = objectMapper.readValue(response.body(), Map.class); + return result; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new EndeeException("Failed to get neighbors", e); + } + } + // ── delete ────────────────────────────────────────────────────────────────── /** Deletes a single object by ID. */ @@ -649,10 +687,10 @@ public Map rebuild(List> fieldSpecs) { } } - /** Returns the current rebuild status. */ + /** Returns the current rebuild status (database-level). */ public Map rebuildStatus() { try { - HttpRequest request = buildGetRequest("/collection/" + name + "/rebuild/status"); + HttpRequest request = buildGetRequest("/status/rebuild"); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); diff --git a/src/main/java/io/endee/client/Endee.java b/src/main/java/io/endee/client/Endee.java index 33a3eb2..59fcc84 100644 --- a/src/main/java/io/endee/client/Endee.java +++ b/src/main/java/io/endee/client/Endee.java @@ -314,9 +314,14 @@ public Map backupInfo(String backupName) { return call("GET", "/backup/" + backupName + "/info", null, Set.of(200)); } - /** Gets the in-progress backup status. */ - public Map activeBackup() { - return call("GET", "/backup/active", null, Set.of(200)); + /** Gets the current backup status. */ + public Map backupStatus() { + return call("GET", "/status/backup", null, Set.of(200)); + } + + /** Gets the current restore status. */ + public Map restoreStatus() { + return call("GET", "/status/restore", null, Set.of(200)); } /** Restores a backup into a new collection. */ @@ -386,13 +391,14 @@ public String downloadBackup(String backupName, String destPath) { } /** - * Uploads a backup .tar file via multipart. + * Uploads a backup .tar file. * * @param filePath path to a .tar backup file + * @param backupName optional name for the backup (defaults to filename without extension) * @return server response */ @SuppressWarnings("unchecked") - public Map uploadBackup(String filePath) { + public Map uploadBackup(String filePath, String backupName) { requireNonEmpty(filePath, "file_path"); Path path = Path.of(filePath); String fileName = path.getFileName().toString(); @@ -400,18 +406,23 @@ public Map uploadBackup(String filePath) { throw new IllegalArgumentException("backup file must be a .tar"); } + String name = + (backupName != null && !backupName.isEmpty()) + ? backupName + : fileName.substring(0, fileName.length() - 4); + try { byte[] fileBytes = Files.readAllBytes(path); - String boundary = "----EndeeBackupBoundary" + System.nanoTime(); - byte[] multipartBody = buildMultipartBody(boundary, "backup", fileName, fileBytes); + String url = + baseUrl + "/backup/upload?name=" + URLEncoder.encode(name, StandardCharsets.UTF_8); HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + "/backup/upload")) + .uri(URI.create(url)) .timeout(DEFAULT_TIMEOUT) - .header("Content-Type", "multipart/form-data; boundary=" + boundary) - .POST(HttpRequest.BodyPublishers.ofByteArray(multipartBody)); + .header("Content-Type", "application/x-tar") + .POST(HttpRequest.BodyPublishers.ofByteArray(fileBytes)); if (token != null && !token.isEmpty()) { builder.header("Authorization", token); @@ -439,24 +450,9 @@ public Map uploadBackup(String filePath) { } } - private static byte[] buildMultipartBody( - String boundary, String fieldName, String fileName, byte[] fileBytes) throws IOException { - String CRLF = "\r\n"; - var baos = new java.io.ByteArrayOutputStream(); - baos.write(("--" + boundary + CRLF).getBytes(StandardCharsets.UTF_8)); - baos.write( - ("Content-Disposition: form-data; name=\"" - + fieldName - + "\"; filename=\"" - + fileName - + "\"" - + CRLF) - .getBytes(StandardCharsets.UTF_8)); - baos.write(("Content-Type: application/x-tar" + CRLF).getBytes(StandardCharsets.UTF_8)); - baos.write(CRLF.getBytes(StandardCharsets.UTF_8)); - baos.write(fileBytes); - baos.write((CRLF + "--" + boundary + "--" + CRLF).getBytes(StandardCharsets.UTF_8)); - return baos.toByteArray(); + /** Uploads a backup .tar file (name derived from filename). */ + public Map uploadBackup(String filePath) { + return uploadBackup(filePath, null); } // ── Internal HTTP helpers ─────────────────────────────────────────────────── From 913c28b5fe9652b2dcf94693aa4ba11c8c3e80ef Mon Sep 17 00:00:00 2001 From: Pankaj Singh Date: Thu, 13 Aug 2026 16:04:55 +0530 Subject: [PATCH 2/2] bump to version 2.1.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9bfaf07..114871c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.endee endee-java-client - 2.0.1-SNAPSHOT + 2.1.0-SNAPSHOT jar Endee Java Client