Java Vector Database Integrations with Future AGI Tracing

Trace vector database operations in Java. Qdrant, Milvus, ChromaDB, Weaviate, MongoDB, Redis, pgvector, Azure AI Search, and Elasticsearch.

📝
TL;DR
  • Elasticsearch uses native OpenTelemetry by default; optional wrappers add retrieval metadata
  • Other integrations below use the Traced<X>(client) pattern
  • Wrapper search/query operations use RETRIEVER span kind
  • Wrapper write operations (upsert, insert, delete) use VECTOR_DB span kind
  • Span attributes vary by database and operation

Prerequisites

For Elasticsearch, follow the self-contained native setup below. For the other wrappers, complete the Java SDK setup first. For Pinecone, see the dedicated Pinecone page.


Qdrant

Wraps io.qdrant.client.QdrantClient. All operations are async internally (the wrapper calls .get() on futures).

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-qdrant</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-qdrant:main-SNAPSHOT'
import ai.traceai.qdrant.TracedQdrantClient;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;

QdrantClient client = new QdrantClient(
    QdrantGrpcClient.newBuilder("localhost", 6334, false).build()
);
TracedQdrantClient traced = new TracedQdrantClient(client);

// Search
var results = traced.search("my-collection", queryVector, 10);

// Upsert
traced.upsert("my-collection", pointsList);

// Create collection
traced.createCollection("my-collection", 1536, Distance.Cosine);

Spans: “Qdrant Search” (RETRIEVER), “Qdrant Upsert” (VECTOR_DB), “Qdrant Create Collection” (VECTOR_DB), “Qdrant Delete” (VECTOR_DB), “Qdrant Get” (VECTOR_DB), “Qdrant List Collections” (VECTOR_DB)

Open your project in Future AGI to inspect the spans.

Extra attributes: qdrant.top_score, qdrant.has_filter, qdrant.distance, qdrant.status


Milvus

Wraps io.milvus.v2.client.MilvusClientV2. Uses SDK v2 request objects throughout.

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-milvus</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-milvus:main-SNAPSHOT'
import ai.traceai.milvus.TracedMilvusClient;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.service.vector.request.*;

MilvusClientV2 client = new MilvusClientV2(/* config */);
TracedMilvusClient traced = new TracedMilvusClient(client);

// ANN search
var results = traced.search(SearchReq.builder()
    .collectionName("my-collection")
    .data(List.of(queryVector))
    .topK(10)
    .build());

// Scalar/filtered query
var queryResults = traced.query(QueryReq.builder()
    .collectionName("my-collection")
    .filter("category == 'science'")
    .build());

// Insert
traced.insert(InsertReq.builder()
    .collectionName("my-collection")
    .data(documents)
    .build());

Spans: “Milvus Search” (RETRIEVER), “Milvus Query” (RETRIEVER), “Milvus Insert” (VECTOR_DB), “Milvus Upsert” (VECTOR_DB), “Milvus Delete” (VECTOR_DB), “Milvus Get” (VECTOR_DB)

Extra attributes: milvus.top_score, milvus.filter, milvus.inserted_count, milvus.query_vectors_count


ChromaDB

Wraps tech.amikos.chromadb.Collection. Text-based queries only (the SDK v0.1.7 doesn’t support raw vector queries).

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-chromadb</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-chromadb:main-SNAPSHOT'
import ai.traceai.chromadb.TracedChromaCollection;
import tech.amikos.chromadb.Collection;

Collection collection = /* get from ChromaDB client */;
TracedChromaCollection traced = new TracedChromaCollection(collection, "my-collection");

// Query by text
var results = traced.query(
    List.of("What is machine learning?"),  // query texts
    10,                                     // nResults
    null,                                   // where filter
    null,                                   // whereDocument filter
    List.of(IncludeEnum.DOCUMENTS, IncludeEnum.DISTANCES)
);

// Add documents
traced.add(embeddings, metadatas, documents, ids);

Spans: “ChromaDB Query” (RETRIEVER), “ChromaDB Add” (VECTOR_DB), “ChromaDB Upsert” (VECTOR_DB), “ChromaDB Delete” (VECTOR_DB), “ChromaDB Get” (VECTOR_DB), “ChromaDB Count” (VECTOR_DB)

Extra attributes: chromadb.top_distance (distance, not similarity score - ChromaDB is distance-based)


Weaviate

Wraps io.weaviate.client.WeaviateClient. Uses “class name” terminology instead of “collection”.

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-weaviate</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-weaviate:main-SNAPSHOT'
import ai.traceai.weaviate.TracedWeaviateClient;
import io.weaviate.client.WeaviateClient;

WeaviateClient client = new WeaviateClient(/* config */);
TracedWeaviateClient traced = new TracedWeaviateClient(client);

// Vector search (uses Float[] not List<Float>)
var results = traced.nearVectorSearch("Article", vectorArray, 10, "title", "content");

// Create object
traced.createObject("Article", properties, vectorArray);

// Batch import (varargs - pass individual objects or convert list to array)
traced.batchImport(obj1, obj2, obj3);

Spans: “Weaviate NearVector Search” (RETRIEVER), “Weaviate Create Object” (VECTOR_DB), “Weaviate Batch Import” (VECTOR_DB), “Weaviate Delete Object” (VECTOR_DB), “Weaviate Get Object” (VECTOR_DB)

Extra attributes: weaviate.object_id, weaviate.imported_count, weaviate.has_errors


Wraps com.mongodb.client.MongoCollection<Document>. Builds the $vectorSearch aggregation pipeline internally.

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-mongodb</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-mongodb:main-SNAPSHOT'
import ai.traceai.mongodb.TracedMongoVectorSearch;
import com.mongodb.client.MongoCollection;
import org.bson.Document;

MongoCollection<Document> collection = /* your MongoDB collection */;
TracedMongoVectorSearch traced = new TracedMongoVectorSearch(collection, "my-collection");

// Vector search (uses List<Double>, not List<Float>)
var results = traced.vectorSearch(
    queryVectorDoubles,  // List<Double>
    "embedding",         // vector field path
    "vector_index",      // Atlas Search index name
    10,                  // limit
    100                  // numCandidates
);

// Insert
traced.insertOne(new Document("text", "hello").append("embedding", vectorDoubles));

Spans: “MongoDB Vector Search” (RETRIEVER), “MongoDB Insert” (VECTOR_DB), “MongoDB Insert Many” (VECTOR_DB), “MongoDB Delete” (VECTOR_DB)

Extra attributes: mongodb.num_candidates, mongodb.path, mongodb.top_score

Note: the wrapper constructs the $vectorSearch aggregation pipeline for you and appends vectorSearchScore to results.


Redis

Wraps redis.clients.jedis.JedisPooled. Builds KNN query strings and handles byte conversion internally.

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-redis</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-redis:main-SNAPSHOT'
import ai.traceai.redis.TracedRedisVectorSearch;
import redis.clients.jedis.JedisPooled;

JedisPooled jedis = new JedisPooled("localhost", 6379);
TracedRedisVectorSearch traced = new TracedRedisVectorSearch(jedis);

// Create index
traced.createIndex("my-index", "embedding", 1536, "FLOAT32", "COSINE");

// Add document (float[] for vector)
traced.addDocument("doc:1", vectorArray, Map.of("title", "Hello"));

// Search (float[] for query vector)
var results = traced.vectorSearch("my-index", queryVectorArray, 10);

Spans: “Redis Create Index” (VECTOR_DB), “Redis Vector Search” (RETRIEVER), “Redis Add Document” (VECTOR_DB), “Redis Delete Document” (VECTOR_DB)

Extra attributes: redis.vector_field, redis.distance_metric, redis.algorithm


pgvector

Wraps javax.sql.DataSource or java.sql.Connection directly. Handles table creation, indexing, search with all three distance functions, and batch operations.

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-pgvector</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-pgvector:main-SNAPSHOT'
import ai.traceai.pgvector.TracedPgVectorStore;
import javax.sql.DataSource;

DataSource ds = /* your PostgreSQL DataSource */;
TracedPgVectorStore traced = new TracedPgVectorStore(ds);

// Create table
traced.createTable("documents", 1536);

// Create index (supports ivfflat and hnsw)
traced.createIndex("documents", "hnsw", 100);

// Insert
traced.insert("documents", "doc-1", vectorArray, Map.of("title", "Hello"));

// Search (supports L2, cosine, inner product)
var results = traced.search("documents", queryVectorArray, 10, "cosine");

// Search with filter
var filtered = traced.searchWithFilter("documents", queryVectorArray, 10, "cosine", "metadata->>'title' = 'Hello'");

Warning

searchWithFilter inserts the filter into SQL without binding its values. Use only trusted, fixed SQL; never concatenate untrusted input into the filter.

Spans: “PgVector Search” (RETRIEVER), “PgVector Insert” (VECTOR_DB), “PgVector Batch Insert” (VECTOR_DB), “PgVector Create Table” (VECTOR_DB), “PgVector Create Index” (VECTOR_DB), plus delete, count, and drop operations.

Extra attributes: pgvector.distance_function, pgvector.index_type, pgvector.has_filter

Distance operators: <-> (L2), <=> (cosine), <#> (inner product)


Wraps com.azure.search.documents.SearchClient. The only vector DB with hybrid (text + vector) search support.

<dependency>
    <groupId>com.github.future-agi.traceAI</groupId>
    <artifactId>traceai-java-azure-search</artifactId>
    <version>main-SNAPSHOT</version>
</dependency>
implementation 'com.github.future-agi.traceAI:traceai-java-azure-search:main-SNAPSHOT'
import ai.traceai.azure.search.TracedSearchClient;
import com.azure.search.documents.SearchClient;

SearchClient searchClient = /* build with Azure credentials */;
TracedSearchClient traced = new TracedSearchClient(searchClient, "my-index");

// Pure vector search
var results = traced.searchWithVector("", queryVector, "contentVector", 10);

// Hybrid search (text + vector)
var hybrid = traced.hybridSearch("machine learning", queryVector, "contentVector", 10);

// Text-only search
var textResults = traced.search("machine learning", 10);

// Upload documents
traced.uploadDocuments(documents);

Spans: “Azure Search Vector Query” (RETRIEVER), “Azure Search Hybrid Query” (RETRIEVER), “Azure Search Text Query” (RETRIEVER), “Azure Search Upload Documents” (VECTOR_DB), plus merge, delete, get, and count operations.

Extra attributes: azure_search.search_mode (vector/hybrid/text), azure_search.top_score, azure_search.success_count, azure_search.failed_count


Elasticsearch

Use the Elasticsearch Java client’s native OpenTelemetry instrumentation to send synchronous and asynchronous client operations to Future AGI. You do not need a TracedElasticsearchClient wrapper for native database spans.

Install dependencies

Use Java 17 or newer and an Elasticsearch index you can search. The example uses the 8.12.0 client; match the client major version to your Elasticsearch cluster. For a 9.x cluster, set both elasticsearch-java and elasticsearch-rest-client to 9.5.5. This example explicitly supplies the REST transport dependency so the same RestClientTransport setup works on both client versions.

Add these dependencies inside your Maven project’s <dependencies> element. All are available from Maven Central; no JitPack repository is needed.

<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.12.0</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>8.12.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.1</version>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk</artifactId>
    <version>1.35.0</version>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
    <version>1.35.0</version>
</dependency>

For Gradle, use the same coordinates with mavenCentral():

implementation 'co.elastic.clients:elasticsearch-java:8.12.0'
implementation 'org.elasticsearch.client:elasticsearch-rest-client:8.12.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1'
implementation 'io.opentelemetry:opentelemetry-sdk:1.35.0'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.35.0'

Configure credentials

Set FI_API_KEY and FI_SECRET_KEY to your Future AGI API credentials. The Elasticsearch API key is separate and must have read access to the selected index.

export FI_API_KEY="your-future-agi-api-key"
export FI_SECRET_KEY="your-future-agi-secret-key"
export FI_PROJECT_NAME="elasticsearch-search"
export ELASTICSEARCH_URL="https://your-elasticsearch-host:9200"
export ELASTICSEARCH_API_KEY="your-encoded-elasticsearch-api-key"
export ELASTICSEARCH_INDEX="documents"
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://api.futureagi.com/tracer/v1/traces"

For self-hosted Future AGI, change the traces endpoint to your deployment’s full /tracer/v1/traces URL. Keep the Elasticsearch URL free of embedded credentials and use your JVM’s trusted certificate configuration; do not disable TLS verification.

Save this as src/main/java/NativeElasticsearchExample.java in your Maven project. Run it using your IDE or your application’s normal Java launch command with the dependencies on the classpath.

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.instrumentation.OpenTelemetryForElasticsearch;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import org.apache.http.HttpHost;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import java.util.Map;

public class NativeElasticsearchExample {
    public static void main(String[] args) throws Exception {
        String project = System.getenv("FI_PROJECT_NAME");
        var exporter = OtlpHttpSpanExporter.builder()
            .setEndpoint(System.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"))
            .addHeader("X-Api-Key", System.getenv("FI_API_KEY"))
            .addHeader("X-Secret-Key", System.getenv("FI_SECRET_KEY"))
            .build();
        var provider = SdkTracerProvider.builder()
            .setResource(Resource.getDefault().merge(Resource.create(Attributes.builder()
                .put("service.name", "elasticsearch-search")
                .put("project_name", project)
                .put("project_type", "observe")
                .build())))
            .addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
            .build();

        // This SDK exports traces only and does not replace the global SDK.
        try (var sdk = OpenTelemetrySdk.builder().setTracerProvider(provider).build()) {
            var restClient = RestClient.builder(HttpHost.create(System.getenv("ELASTICSEARCH_URL")))
                .setDefaultHeaders(new BasicHeader[] {
                    new BasicHeader("Authorization", "ApiKey " + System.getenv("ELASTICSEARCH_API_KEY"))
                })
                .build();
            // false keeps search request bodies out of native span attributes.
            try (var transport = new RestClientTransport(restClient, new JacksonJsonpMapper(),
                    null, new OpenTelemetryForElasticsearch(sdk, false))) {
                var client = new ElasticsearchClient(transport);
                var response = client.search(s -> s
                    .index(System.getenv("ELASTICSEARCH_INDEX"))
                    .query(q -> q.match(m -> m.field("text").query("observability")))
                    .size(5), Map.class);
                System.out.println("Matches returned: " + response.hits().hits().size());
            }
        } // Closing the SDK drains the batch processor before this process exits.
    }
}

For a KNN search, replace the client and search inside the transport block with the following. Your index needs a three-dimensional embedding dense-vector field; use a vector with the dimensions configured in your own index.

var client = new ElasticsearchClient(transport);
var response = client.search(s -> s
    .index(System.getenv("ELASTICSEARCH_INDEX"))
    .knn(k -> k.field("embedding")
        .queryVector(java.util.List.of(0.1f, 0.2f, 0.3f))
        .k(5).numCandidates(100))
    .size(5), Map.class);
System.out.println("Matches returned: " + response.hits().hits().size());

The native transport also traces ElasticsearchAsyncClient. To use it, replace the synchronous client and search inside the transport block with:

var client = new co.elastic.clients.elasticsearch.ElasticsearchAsyncClient(transport);
var response = client.search(s -> s
    .index(System.getenv("ELASTICSEARCH_INDEX"))
    .query(q -> q.match(m -> m.field("text").query("observability")))
    .size(5), Map.class).get(30, java.util.concurrent.TimeUnit.SECONDS);
System.out.println("Matches returned: " + response.hits().hits().size());

If your application already owns an OpenTelemetry SDK, reuse it and configure its exporter instead of creating another provider. Pass it to new OpenTelemetryForElasticsearch(existingSdk, false). Alternatively, register your SDK globally before constructing the transport and use the two-argument RestClientTransport(restClient, mapper) constructor; native tracing uses that global SDK by default. Do not call TraceAI.init() to register a second global provider in an already-instrumented application.

Warning

Disabling search-body capture does not redact all metadata: native spans include index names, request URLs, and error details. Keep secrets out of URL/query parameters and review error data before exporting sensitive workloads. TraceAI’s content flags do not control native Elasticsearch spans.

Inspect your traces

Open Observe → Traces in your Future AGI project. Native spans use the elasticsearch-api instrumentation scope and the CLIENT OpenTelemetry kind. The 8.12.0 client records db.system=elasticsearch and db.operation=search; 9.5.5 uses db.system.name and db.operation.name. These are database operations, not automatically RETRIEVER spans with top-k or result-score metadata.

Optional retrieval metadata

With the 8.12.0 client, keep TracedElasticsearchClient when you need the additional semantic retrieval span for KNN searches. The published wrapper 1.0.1 targets 8.12.0 and is not binary-compatible with the 9.x KNN API; use native tracing alone on 9.x. Add the published package from Maven Central:

<dependency>
    <groupId>com.futureagi</groupId>
    <artifactId>traceai-java-elasticsearch</artifactId>
    <version>1.0.1</version>
</dependency>
implementation 'com.futureagi:traceai-java-elasticsearch:1.0.1'

Inside the example’s transport block, use the same SDK for both the wrapper and native client. Replace the search with the following code; the index must have a three-dimensional embedding dense-vector field:

var traced = new ai.traceai.elasticsearch.TracedElasticsearchClient(
    client, new ai.traceai.FITracer(sdk.getTracer("elasticsearch-retrieval")));
var results = traced.knnSearch(System.getenv("ELASTICSEARCH_INDEX"),
    new float[] {0.1f, 0.2f, 0.3f}, 5, 100, "embedding");
System.out.println("Matches returned: " + results.hits().hits().size());

This produces a semantic RETRIEVER parent and a native database child for one search. The parent adds retriever.top_k, embedding.dimensions, db.vector.results.count, and elasticsearch.top_score. It is optional enrichment, not a replacement for native tracing of the rest of the client API. Do not add another manual retrieval span around the wrapper for the same operation.

Existing constructors and knnSearchWithFilter, index, bulkIndex, delete, createIndex, and unwrap remain available. Native tracing alone is sufficient when you do not need these semantic wrapper spans.

Troubleshooting

  • No traces: check both Future AGI headers, the project_name resource, and the full traces endpoint. Wait for the batch to export, and close the SDK on shutdown. With a global SDK, initialize it before creating your Elasticsearch transport and do not disable OTEL_INSTRUMENTATION_ELASTICSEARCH_ENABLED.
  • Two search-related spans: the optional wrapper intentionally adds a retrieval parent. Use the native client alone for one database span per request. If you also use a Java agent, avoid configuring overlapping Elasticsearch instrumentation or a second exporter pipeline.
  • Elasticsearch rejects the request: match your client major version to the server, confirm API-key read access, and check the index name and vector mapping.

Common span attributes

Depending on the wrapper and operation, spans may include:

AttributeDescription
db.systemDatabase name (e.g., pinecone, qdrant, milvus)
db.vector.collection.name or db.vector.index.nameCollection or index name
embedding.dimensionsVector dimensions
retriever.top_kNumber of results requested (search operations)
db.vector.results.countNumber of results returned
Was this page helpful?

Questions & Discussion