From 21ede49529d42ab44a8fb962ec42f5e4d11bd112 Mon Sep 17 00:00:00 2001
From: Rashin Arab <3806658+rasharab@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:15:21 -0700
Subject: [PATCH 1/3] feat: add response metadata fields for read units,
upserted count, and m
Agent-Id: agent-99b8efc1-3adb-4099-8683-03d0a3351bed
Linked-Note-Id: b55dbb0b-4a64-437e-93bd-f29d287c9b0c
---
.../io/pinecone/configs/ResponseMetadata.java | 60 +++++++++
.../configs/ResponseMetadataInterceptor.java | 71 +++++++++++
.../configs/ResponseMetadataTest.java | 116 ++++++++++++++++++
3 files changed, 247 insertions(+)
diff --git a/src/main/java/io/pinecone/configs/ResponseMetadata.java b/src/main/java/io/pinecone/configs/ResponseMetadata.java
index 3b4f14dd..60001540 100644
--- a/src/main/java/io/pinecone/configs/ResponseMetadata.java
+++ b/src/main/java/io/pinecone/configs/ResponseMetadata.java
@@ -9,6 +9,9 @@
*
{@link #getClientDurationMs()} - Total round-trip time measured by the client
* {@link #getServerDurationMs()} - Server processing time from x-pinecone-response-duration-ms header
* {@link #getNetworkOverheadMs()} - Computed network + serialization overhead
+ * {@link #getReadUnits()} - Number of read units consumed (query, fetch, list operations)
+ * {@link #getUpsertedCount()} - Number of records upserted (upsert operations)
+ * {@link #getMatchedRecords()} - Number of records matched (update operations)
*
*
* Example usage with a listener:
@@ -34,6 +37,9 @@ public class ResponseMetadata {
private final String status;
private final String grpcStatusCode;
private final String errorType;
+ private final Integer readUnits;
+ private final Integer upsertedCount;
+ private final Integer matchedRecords;
private ResponseMetadata(Builder builder) {
this.operationName = builder.operationName;
@@ -45,6 +51,9 @@ private ResponseMetadata(Builder builder) {
this.status = builder.status;
this.grpcStatusCode = builder.grpcStatusCode;
this.errorType = builder.errorType;
+ this.readUnits = builder.readUnits;
+ this.upsertedCount = builder.upsertedCount;
+ this.matchedRecords = builder.matchedRecords;
}
/**
@@ -134,6 +143,30 @@ public String getErrorType() {
return errorType;
}
+ /**
+ * Returns the number of read units consumed by the operation, or null if not applicable.
+ * Populated for query, fetch, and list operations.
+ */
+ public Integer getReadUnits() {
+ return readUnits;
+ }
+
+ /**
+ * Returns the number of records upserted, or null if not applicable.
+ * Populated for upsert operations.
+ */
+ public Integer getUpsertedCount() {
+ return upsertedCount;
+ }
+
+ /**
+ * Returns the number of records matched by the operation, or null if not applicable.
+ * Populated for update operations.
+ */
+ public Integer getMatchedRecords() {
+ return matchedRecords;
+ }
+
/**
* Returns true if the operation was successful.
*/
@@ -162,6 +195,15 @@ public String toString() {
if (errorType != null) {
sb.append(", errorType=").append(errorType);
}
+ if (readUnits != null) {
+ sb.append(", readUnits=").append(readUnits);
+ }
+ if (upsertedCount != null) {
+ sb.append(", upsertedCount=").append(upsertedCount);
+ }
+ if (matchedRecords != null) {
+ sb.append(", matchedRecords=").append(matchedRecords);
+ }
sb.append("}");
return sb.toString();
}
@@ -176,6 +218,9 @@ public static class Builder {
private String status = "success";
private String grpcStatusCode = "OK";
private String errorType;
+ private Integer readUnits;
+ private Integer upsertedCount;
+ private Integer matchedRecords;
public Builder operationName(String operationName) {
this.operationName = operationName;
@@ -222,6 +267,21 @@ public Builder errorType(String errorType) {
return this;
}
+ public Builder readUnits(Integer readUnits) {
+ this.readUnits = readUnits;
+ return this;
+ }
+
+ public Builder upsertedCount(Integer upsertedCount) {
+ this.upsertedCount = upsertedCount;
+ return this;
+ }
+
+ public Builder matchedRecords(Integer matchedRecords) {
+ this.matchedRecords = matchedRecords;
+ return this;
+ }
+
public ResponseMetadata build() {
return new ResponseMetadata(this);
}
diff --git a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java
index 12774d14..c7baccf7 100644
--- a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java
+++ b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java
@@ -75,6 +75,9 @@ public void start(Listener responseListener, Metadata headers) {
responseListener) {
private Metadata initialHeaders;
+ private Integer readUnits;
+ private Integer upsertedCount;
+ private Integer matchedRecords;
@Override
public void onHeaders(Metadata headers) {
@@ -86,6 +89,19 @@ public void onHeaders(Metadata headers) {
super.onHeaders(headers);
}
+ @Override
+ public void onMessage(RespT message) {
+ // Extract response body fields via reflection
+ try {
+ readUnits = extractReadUnits(message);
+ upsertedCount = extractUpsertedCount(message);
+ matchedRecords = extractMatchedRecords(message);
+ } catch (Exception e) {
+ logger.debug("Could not extract response body fields", e);
+ }
+ super.onMessage(message);
+ }
+
@Override
public void onClose(Status status, Metadata trailers) {
try {
@@ -116,6 +132,9 @@ public void onClose(Status status, Metadata trailers) {
.status(statusStr)
.grpcStatusCode(status.getCode().name())
.errorType(errorType)
+ .readUnits(readUnits)
+ .upsertedCount(upsertedCount)
+ .matchedRecords(matchedRecords)
.build();
invokeListener(metadata);
@@ -159,6 +178,58 @@ private String extractNamespace(T message) {
}
}
+ private Integer extractReadUnits(T message) {
+ // Extract read units from QueryResponse, FetchResponse, ListResponse via hasUsage()/getUsage()/getReadUnits()
+ try {
+ java.lang.reflect.Method hasUsage = message.getClass().getMethod("hasUsage");
+ Boolean has = (Boolean) hasUsage.invoke(message);
+ if (Boolean.TRUE.equals(has)) {
+ java.lang.reflect.Method getUsage = message.getClass().getMethod("getUsage");
+ Object usage = getUsage.invoke(message);
+ java.lang.reflect.Method getReadUnits = usage.getClass().getMethod("getReadUnits");
+ Object result = getReadUnits.invoke(usage);
+ return ((Number) result).intValue();
+ }
+ } catch (NoSuchMethodException e) {
+ // Response type does not have usage field
+ } catch (Exception e) {
+ logger.debug("Could not extract read units from response", e);
+ }
+ return null;
+ }
+
+ private Integer extractUpsertedCount(T message) {
+ // Extract upserted count from UpsertResponse via getUpsertedCount()
+ try {
+ java.lang.reflect.Method getUpsertedCount = message.getClass().getMethod("getUpsertedCount");
+ Object result = getUpsertedCount.invoke(message);
+ return ((Number) result).intValue();
+ } catch (NoSuchMethodException e) {
+ // Response type does not have upsertedCount field
+ } catch (Exception e) {
+ logger.debug("Could not extract upserted count from response", e);
+ }
+ return null;
+ }
+
+ private Integer extractMatchedRecords(T message) {
+ // Extract matched records from UpdateResponse via hasMatchedRecords()/getMatchedRecords()
+ try {
+ java.lang.reflect.Method hasMatchedRecords = message.getClass().getMethod("hasMatchedRecords");
+ Boolean has = (Boolean) hasMatchedRecords.invoke(message);
+ if (Boolean.TRUE.equals(has)) {
+ java.lang.reflect.Method getMatchedRecords = message.getClass().getMethod("getMatchedRecords");
+ Object result = getMatchedRecords.invoke(message);
+ return ((Number) result).intValue();
+ }
+ } catch (NoSuchMethodException e) {
+ // Response type does not have matchedRecords field
+ } catch (Exception e) {
+ logger.debug("Could not extract matched records from response", e);
+ }
+ return null;
+ }
+
private String mapGrpcStatusToErrorType(Status.Code code) {
switch (code) {
case INVALID_ARGUMENT:
diff --git a/src/test/java/io/pinecone/configs/ResponseMetadataTest.java b/src/test/java/io/pinecone/configs/ResponseMetadataTest.java
index 1695dd4e..5547e2a0 100644
--- a/src/test/java/io/pinecone/configs/ResponseMetadataTest.java
+++ b/src/test/java/io/pinecone/configs/ResponseMetadataTest.java
@@ -17,6 +17,9 @@ void testBuilderWithAllFields() {
.serverDurationMs(100L)
.status("success")
.grpcStatusCode("OK")
+ .readUnits(5)
+ .upsertedCount(10)
+ .matchedRecords(3)
.build();
assertEquals("query", metadata.getOperationName());
@@ -29,6 +32,9 @@ void testBuilderWithAllFields() {
assertEquals("OK", metadata.getGrpcStatusCode());
assertTrue(metadata.isSuccess());
assertNull(metadata.getErrorType());
+ assertEquals(5, metadata.getReadUnits());
+ assertEquals(10, metadata.getUpsertedCount());
+ assertEquals(3, metadata.getMatchedRecords());
}
@Test
@@ -180,5 +186,115 @@ void testAllErrorTypes() {
assertFalse(metadata.isSuccess());
}
}
+
+ @Test
+ void testReadUnitsNullByDefault() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("query")
+ .build();
+
+ assertNull(metadata.getReadUnits());
+ }
+
+ @Test
+ void testReadUnitsWithValue() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("query")
+ .readUnits(10)
+ .build();
+
+ assertEquals(10, metadata.getReadUnits());
+ }
+
+ @Test
+ void testUpsertedCountNullByDefault() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("upsert")
+ .build();
+
+ assertNull(metadata.getUpsertedCount());
+ }
+
+ @Test
+ void testUpsertedCountWithValue() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("upsert")
+ .upsertedCount(100)
+ .build();
+
+ assertEquals(100, metadata.getUpsertedCount());
+ }
+
+ @Test
+ void testMatchedRecordsNullByDefault() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("update")
+ .build();
+
+ assertNull(metadata.getMatchedRecords());
+ }
+
+ @Test
+ void testMatchedRecordsWithValue() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("update")
+ .matchedRecords(42)
+ .build();
+
+ assertEquals(42, metadata.getMatchedRecords());
+ }
+
+ @Test
+ void testToStringWithReadUnits() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("query")
+ .indexName("my-index")
+ .clientDurationMs(100)
+ .readUnits(5)
+ .build();
+
+ String str = metadata.toString();
+ assertTrue(str.contains("readUnits=5"));
+ }
+
+ @Test
+ void testToStringWithUpsertedCount() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("upsert")
+ .indexName("my-index")
+ .clientDurationMs(100)
+ .upsertedCount(50)
+ .build();
+
+ String str = metadata.toString();
+ assertTrue(str.contains("upsertedCount=50"));
+ }
+
+ @Test
+ void testToStringWithMatchedRecords() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("update")
+ .indexName("my-index")
+ .clientDurationMs(100)
+ .matchedRecords(7)
+ .build();
+
+ String str = metadata.toString();
+ assertTrue(str.contains("matchedRecords=7"));
+ }
+
+ @Test
+ void testToStringOmitsNullResponseBodyFields() {
+ ResponseMetadata metadata = ResponseMetadata.builder()
+ .operationName("delete")
+ .indexName("my-index")
+ .clientDurationMs(100)
+ .build();
+
+ String str = metadata.toString();
+ assertFalse(str.contains("readUnits"));
+ assertFalse(str.contains("upsertedCount"));
+ assertFalse(str.contains("matchedRecords"));
+ }
}
From a401a7d49ad343cc9a75894e768cb37a59f7aeb3 Mon Sep 17 00:00:00 2001
From: Rashin Arab <3806658+rasharab@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:19:35 -0700
Subject: [PATCH 2/3] fix: add List operation to tracked operations in
ResponseMetadataInterce
Agent-Id: agent-e63157ed-432d-4883-9f3e-1895a0b24e7a
---
.../java/io/pinecone/configs/ResponseMetadataInterceptor.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java
index c7baccf7..e3fb7139 100644
--- a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java
+++ b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java
@@ -21,7 +21,7 @@ public class ResponseMetadataInterceptor implements ClientInterceptor {
// Operations to track (matches VectorService RPC method names)
private static final Set TRACKED_OPERATIONS = new HashSet<>(Arrays.asList(
- "Upsert", "Query", "Fetch", "Update", "Delete"
+ "Upsert", "Query", "Fetch", "Update", "Delete", "List"
));
private final ResponseMetadataListener listener;
From a164deeb6f77fecb194e1638307067bfb71e614d Mon Sep 17 00:00:00 2001
From: Rashin Arab <3806658+rasharab@users.noreply.github.com>
Date: Fri, 27 Mar 2026 11:22:40 -0700
Subject: [PATCH 3/3] docs: update README with usage and operation-result
metadata fields
Agent-Id: agent-3f21d110-2d0f-42e3-aae2-b6331803d85a
---
README.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/README.md b/README.md
index 4b07f152..6ecae0a3 100644
--- a/README.md
+++ b/README.md
@@ -168,12 +168,24 @@ Pinecone pinecone = new Pinecone.Builder("PINECONE_API_KEY")
metadata.getClientDurationMs(),
metadata.getServerDurationMs(),
metadata.getNetworkOverheadMs());
+
+ // Log usage and operation-result fields when available
+ if (metadata.getReadUnits() != null) {
+ System.out.printf(" Read units consumed: %d%n", metadata.getReadUnits());
+ }
+ if (metadata.getUpsertedCount() != null) {
+ System.out.printf(" Upserted count: %d%n", metadata.getUpsertedCount());
+ }
+ if (metadata.getMatchedRecords() != null) {
+ System.out.printf(" Matched records: %d%n", metadata.getMatchedRecords());
+ }
})
.build();
Index index = pinecone.getIndexConnection("example-index");
index.query(5, Arrays.asList(1.0f, 2.0f, 3.0f));
// Output: Operation: query | Client: 45ms | Server: 32ms | Network: 13ms
+// Read units consumed: 5
```
The `ResponseMetadata` object provides:
@@ -189,6 +201,9 @@ The `ResponseMetadata` object provides:
| `isSuccess()` | Whether the operation succeeded |
| `getGrpcStatusCode()` | gRPC status code |
| `getErrorType()` | Error category when failed |
+| `getReadUnits()` | Read units consumed (available on query, fetch, list) |
+| `getUpsertedCount()` | Number of vectors upserted (available on upsert) |
+| `getMatchedRecords()` | Number of matched records (available on update) |
For a complete OpenTelemetry integration example with Prometheus and Grafana, see the [java-otel-metrics example](examples/java-otel-metrics/).