-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(search): add Google Gemini embedding provider #27974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pmbrull
wants to merge
24
commits into
main
Choose a base branch
from
pmbrull/gcp-embedding-client
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
800bd8d
Add design: Google Gemini embedding client
pmbrull 9caae6c
Add implementation plan: Google Gemini embedding client
pmbrull 4d389e9
feat(spec): add google embedding provider config block
pmbrull ed28632
feat(search): add GoogleEmbeddingClient with happy-path test
pmbrull e0aa250
refactor(search): extract MODELS_PREFIX constant in GoogleEmbeddingCl…
pmbrull 544f8b1
test(search): add constructor validation tests for GoogleEmbeddingClient
pmbrull c2c04e8
test(search): add blank model id test and clarify null-modelId workar…
pmbrull 0a5e281
test(search): add HTTP error and malformed response tests for GoogleE…
pmbrull bf39f6c
test(search): tighten empty values array assertion to check message
pmbrull 257f92c
test(search): verify Google embedding request URL, headers, and body …
pmbrull 399b3a7
test(search): extract endpoint constant and harden extractBody helper
pmbrull 82342c9
feat(search): wire google embedding provider into SearchRepository sw…
pmbrull 69912b7
test(search): cover null dimension and custom endpoint, drop redundan…
pmbrull a61bf0a
Update generated TypeScript types
github-actions[bot] e91c90f
Remove internal planning docs from PR
pmbrull 7870e5c
Address PR review comments
pmbrull 8174348
Update generated TypeScript types
github-actions[bot] 69a5fb2
Wire google embedding provider into openmetadata.yaml defaults
pmbrull 0b112df
Use gemini-embedding-001 default and pass outputDimensionality
pmbrull 1581619
Update generated TypeScript types
github-actions[bot] a787b4c
Guard against missing google config in SystemRepository diagnostic
pmbrull e7c36ab
Validate google.endpoint contains :embedContent at construction
pmbrull 30ddc3d
feat(spec): add modelId chat field to google block
pmbrull b00e1e2
Update generated TypeScript types
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
...ce/src/main/java/org/openmetadata/service/search/vector/client/GoogleEmbeddingClient.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /* | ||
| * Copyright 2024 Collate | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file | ||
| * except in compliance with the License. You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software distributed under the License | ||
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express | ||
| * or implied. See the License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
| package org.openmetadata.service.search.vector.client; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.fasterxml.jackson.databind.node.ArrayNode; | ||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.net.URLEncoder; | ||
| import java.net.http.HttpClient; | ||
| import java.net.http.HttpRequest; | ||
| import java.net.http.HttpResponse; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.time.Duration; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration; | ||
| import org.openmetadata.schema.service.configuration.elasticsearch.Google; | ||
| import org.openmetadata.schema.service.configuration.elasticsearch.NaturalLanguageSearchConfiguration; | ||
|
|
||
| @Slf4j | ||
| public final class GoogleEmbeddingClient extends EmbeddingClient { | ||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
| private static final String MODELS_PREFIX = "models/"; | ||
| private static final String DEFAULT_BASE_URL = | ||
| "https://generativelanguage.googleapis.com/v1beta/" + MODELS_PREFIX; | ||
|
|
||
| private final HttpClient httpClient; | ||
| private final String apiKey; | ||
| private final String modelId; | ||
| private final int dimension; | ||
| private final String endpoint; | ||
|
|
||
| public GoogleEmbeddingClient(ElasticSearchConfiguration config) { | ||
| super(resolveMaxConcurrent(config)); | ||
| NaturalLanguageSearchConfiguration nlsCfg = config.getNaturalLanguageSearch(); | ||
| Google googleCfg = nlsCfg.getGoogle(); | ||
| if (googleCfg == null) { | ||
| throw new IllegalArgumentException("Google configuration is required"); | ||
| } | ||
| if (googleCfg.getApiKey() == null || googleCfg.getApiKey().isBlank()) { | ||
| throw new IllegalArgumentException("Google API key is required"); | ||
| } | ||
| if (googleCfg.getEmbeddingModelId() == null || googleCfg.getEmbeddingModelId().isBlank()) { | ||
| throw new IllegalArgumentException("Google embedding model ID is required"); | ||
| } | ||
| if (googleCfg.getEmbeddingDimension() == null || googleCfg.getEmbeddingDimension() <= 0) { | ||
| throw new IllegalArgumentException("Google embedding dimension must be positive"); | ||
| } | ||
|
|
||
| this.apiKey = googleCfg.getApiKey(); | ||
| this.modelId = googleCfg.getEmbeddingModelId(); | ||
| this.dimension = googleCfg.getEmbeddingDimension(); | ||
| this.endpoint = resolveEndpoint(googleCfg); | ||
| this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); | ||
|
|
||
| LOG.info( | ||
| "Initialized GoogleEmbeddingClient with model={}, dimension={}, endpoint={}", | ||
| modelId, | ||
| dimension, | ||
| endpoint); | ||
| } | ||
|
|
||
| GoogleEmbeddingClient( | ||
| HttpClient httpClient, String apiKey, String modelId, int dimension, String endpoint) { | ||
| this(httpClient, apiKey, modelId, dimension, endpoint, DEFAULT_MAX_CONCURRENT_REQUESTS); | ||
| } | ||
|
|
||
| GoogleEmbeddingClient( | ||
| HttpClient httpClient, | ||
| String apiKey, | ||
| String modelId, | ||
| int dimension, | ||
| String endpoint, | ||
| int maxConcurrentRequests) { | ||
| super(maxConcurrentRequests); | ||
| this.httpClient = httpClient; | ||
| this.apiKey = apiKey; | ||
| this.modelId = modelId; | ||
| this.dimension = dimension; | ||
| this.endpoint = endpoint; | ||
| } | ||
|
|
||
| private String resolveEndpoint(Google config) { | ||
| String configured = config.getEndpoint(); | ||
| if (configured != null && !configured.isBlank()) { | ||
| return configured.replaceAll("/+$", ""); | ||
|
pmbrull marked this conversation as resolved.
Outdated
|
||
| } | ||
| return DEFAULT_BASE_URL + config.getEmbeddingModelId() + ":embedContent"; | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| protected float[] doEmbed(String text) { | ||
| if (text == null || text.isBlank()) { | ||
| throw new IllegalArgumentException("Input text must not be null or blank"); | ||
| } | ||
|
|
||
| try { | ||
| String body = buildRequestBody(text); | ||
| HttpRequest request = buildRequest(body); | ||
| HttpResponse<String> response = | ||
| httpClient.send(request, HttpResponse.BodyHandlers.ofString()); | ||
|
|
||
| if (response.statusCode() != 200) { | ||
| String errorMsg = extractErrorMessage(response.body()); | ||
| throw new RuntimeException( | ||
| "Google API returned status " + response.statusCode() + ": " + errorMsg); | ||
| } | ||
|
|
||
| return parseEmbeddingResponse(response.body()); | ||
| } catch (IOException e) { | ||
| LOG.error("IO error calling Google API: {}", e.getMessage(), e); | ||
| throw new RuntimeException("Google embedding generation failed due to IO error", e); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new RuntimeException("Google embedding generation was interrupted", e); | ||
| } | ||
| } | ||
|
|
||
| private String buildRequestBody(String text) throws IOException { | ||
| ObjectNode payload = MAPPER.createObjectNode(); | ||
| payload.put("model", MODELS_PREFIX + modelId); | ||
| ObjectNode content = payload.putObject("content"); | ||
| ArrayNode parts = content.putArray("parts"); | ||
| ObjectNode part = parts.addObject(); | ||
| part.put("text", text); | ||
| return MAPPER.writeValueAsString(payload); | ||
| } | ||
|
|
||
| private HttpRequest buildRequest(String body) { | ||
| // Google's Generative Language API requires the API key as a `key=` query parameter; | ||
| // it does not accept Bearer/Authorization headers for AI Studio keys. | ||
| String encodedKey = URLEncoder.encode(apiKey, StandardCharsets.UTF_8); | ||
|
Comment on lines
+151
to
+154
|
||
| String separator = endpoint.contains("?") ? "&" : "?"; | ||
| String url = endpoint + separator + "key=" + encodedKey; | ||
| return HttpRequest.newBuilder() | ||
| .uri(URI.create(url)) | ||
| .header("Content-Type", "application/json") | ||
| .timeout(Duration.ofSeconds(30)) | ||
| .POST(HttpRequest.BodyPublishers.ofString(body)) | ||
| .build(); | ||
|
Comment on lines
+151
to
+162
|
||
| } | ||
|
|
||
| @Override | ||
| public int getDimension() { | ||
| return dimension; | ||
| } | ||
|
|
||
| @Override | ||
| public String getModelId() { | ||
| return modelId; | ||
| } | ||
|
|
||
| private float[] parseEmbeddingResponse(String responseBody) { | ||
| try { | ||
| JsonNode root = MAPPER.readTree(responseBody); | ||
| JsonNode embedding = root.get("embedding"); | ||
| if (embedding == null || !embedding.isObject()) { | ||
| throw new RuntimeException("Invalid Google response: no embedding object found"); | ||
| } | ||
| JsonNode values = embedding.get("values"); | ||
| if (values == null || !values.isArray() || values.isEmpty()) { | ||
| throw new RuntimeException("Invalid Google response: no values array found"); | ||
| } | ||
| float[] result = new float[values.size()]; | ||
| for (int i = 0; i < values.size(); i++) { | ||
| result[i] = (float) values.get(i).asDouble(); | ||
| } | ||
| return result; | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("Failed to parse Google embedding response", e); | ||
| } | ||
| } | ||
|
|
||
| private String extractErrorMessage(String responseBody) { | ||
| try { | ||
| JsonNode root = MAPPER.readTree(responseBody); | ||
| JsonNode error = root.get("error"); | ||
| if (error != null && error.has("message")) { | ||
| return error.get("message").asText(); | ||
| } | ||
| } catch (Exception e) { | ||
| LOG.trace("Could not parse Google error envelope: {}", e.getMessage()); | ||
| } | ||
| return responseBody; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.