feat(embedding): add GeminiEmbeddingClient implementing the EmbeddingClient SPI - #21
Merged
Merged
Conversation
marevol
force-pushed
the
add-content-chunk-embedding-client
branch
from
August 2, 2026 02:17
b2209d3 to
9a8ea7f
Compare
marevol
marked this pull request as ready for review
August 2, 2026 02:19
This was referenced Aug 2, 2026
Merged
Merged
…Client SPI Adds GeminiEmbeddingClient, the Gemini-backed implementation of the EmbeddingClient SPI introduced in Fess core (codelibs/fess#3184), so the content-chunk RAG pipeline can produce vectors through Gemini's batchEmbedContents endpoint. It is registered as geminiEmbeddingClient in fess_llm++.xml alongside the existing GeminiLlmClient. The client extends AbstractEmbeddingClient and defines only what is Gemini-specific. The provider-agnostic members - getDimension(), isContentChunkerEnabled(), getAvailabilityCheckInterval(), the numeric config readers, and the separate connect timeout that keeps a black-holed endpoint from stalling container startup on the init()-time availability probe - come from the base class as shared by codelibs/fess#3202. Client behavior: - embedDocuments() and embedQuery() share one HTTP path and send Gemini's taskType parameter, defaulting to RETRIEVAL_DOCUMENT and RETRIEVAL_QUERY respectively. Both are configurable, and an empty value omits the field. - The input is split into sub-batches of at most 100 requests, the documented batchEmbedContents cap, each embedded by an independent call with its own retry budget, and the vectors are concatenated in input order. Reassembly is all-or-nothing, so a partial result can never corrupt the chunk-to-vector mapping. - outputDimensionality is sent only for MRL-capable models and omitted for the legacy embedding-* line, which rejects it with a non-retryable HTTP 400. Truncated vectors are L2-normalized, which Google requires for the default gemini-embedding-001 at any dimension other than 3072 and which matters because core permits space_type values that are not scale-invariant. - Non-numeric and non-finite vector components are rejected so a poisoned vector never reaches the kNN index, and a non-positive configured dimension is rejected up front. - Retry and timeout semantics mirror this repo's own GeminiLlmClient rather than OllamaEmbeddingClient's conventions: 429/500/503/504 are retried (no 502), and IOException/ParseException are not. A single retry backoff sleep is capped at 60s, so a persistently throttled sub-batch cannot stall the sequential ChunkVectorJob fan-out. - Every content_chunker.embedding.gemini.* key resolves through the inherited getConfigString()/getConfigInt()/getConfigLong(), i.e. conf/system.properties, the single channel core uses for all content_chunker.* settings. rag.llm.gemini.* on GeminiLlmClient is unaffected and stays on fess_config.properties. Credential handling, applied to both the embedding and the LLM client: - GeminiApiUrl is the single definition of the API-URL rules for both clients: the query-string-preserving endpoint path append, the request construction that replaces the URI-rejection exception, and the wording of the userinfo refusal. What counts as a credential inside a URL, and how one is kept out of a log, is provider-agnostic and delegates to core's CredentialUrlUtil (codelibs/fess#3202); that shared implementation also catches the protocol-relative and scheme-less userinfo forms this repo's own scan missed. - Credentials are masked in every URL-bearing log statement, covering both the key=... query parameter form Gemini documents as an alternative to the x-goog-api-key header and RFC 3986 userinfo in the authority. - An api.url whose authority embeds userinfo is refused before a request URI is built from it; such a URL can never issue a request anyway, since the HTTP client rejects it per RFC 9110 section 4.2.4. The availability check fails closed and latches one error naming the setting and the supported alternatives, because it runs from init() as a container post-construct and an escaping exception would abort assembly. The chat, stream, and embed entry points throw the same message instead. - The invalid-URL exception carries only the name of the setting and the parser's own reason and index, never any slice of the URL, and has no cause. - Endpoint paths are spliced in ahead of any query string, so an api.url that carries one still addresses /models and :batchEmbedContents correctly. Documentation: README.md and CLAUDE.md now describe the content_chunker.embedding.gemini.* keys with their correct configuration channel, scope the no-restart claim to the properties that actually honor it, and note the token-quota trap behind the 100-request batch cap.
marevol
force-pushed
the
add-content-chunk-embedding-client
branch
from
August 2, 2026 12:35
d423d02 to
3461013
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Adds
GeminiEmbeddingClient, the Gemini-backed implementation of theEmbeddingClientSPI introduced in Fess core by codelibs/fess#3184 (merged), so the content-chunk RAG pipeline can produce vectors through Gemini'sbatchEmbedContentsendpoint. It is registered asgeminiEmbeddingClientinfess_llm++.xmlalongside the existingGeminiLlmClient.The client extends
AbstractEmbeddingClientand defines only what is Gemini-specific. The provider-agnostic members —getDimension(),isContentChunkerEnabled(),getAvailabilityCheckInterval(), the numeric config readers, and the separate connect timeout — come from the base class as shared by codelibs/fess#3202 (merged). That separate connect timeout matters at startup: the first availability probe runs synchronously frominit(), which the container invokes as an eager init-method, so a black-holed endpoint would otherwise stall Tomcat context startup for the full response timeout.Retry and timeout semantics deliberately mirror this repo's own
GeminiLlmClient(retryable statuses 429/500/503/504, noIOExceptionretry, single-tier response timeout) rather thanOllamaEmbeddingClient's conventions.Client behavior
embedDocuments()andembedQuery()share one HTTP path and send Gemini'staskTypeparameter, defaulting toRETRIEVAL_DOCUMENTandRETRIEVAL_QUERYrespectively. Both are configurable, and an empty value omits the field.batchEmbedContentscap, each embedded by an independent call with its own retry budget, and the vectors are concatenated in input order. Reassembly is all-or-nothing, so a partial result can never corrupt the chunk-to-vector mapping.outputDimensionalityis sent only for MRL-capable models and omitted for the legacyembedding-*line, which rejects it with a non-retryable HTTP 400. Truncated vectors are L2-normalized, which Google requires for the defaultgemini-embedding-001at any dimension other than 3072 and which matters because core permitsspace_typevalues that are not scale-invariant.ChunkVectorJobfan-out.content_chunker.embedding.gemini.*key resolves through the inheritedgetConfigString()/getConfigInt()/getConfigLong(), i.e.conf/system.properties— the single channel core uses for allcontent_chunker.*settings.rag.llm.gemini.*onGeminiLlmClientis unaffected and stays onfess_config.properties.Credential handling (both the embedding and the LLM client)
GeminiApiUrlis the single definition of the API-URL rules for both clients: the query-string-preserving endpoint path append, the request construction that replaces the URI-rejection exception, and the wording of the userinfo refusal. What counts as a credential inside a URL, and how one is kept out of a log, is provider-agnostic and delegates to core'sCredentialUrlUtil(refactor(embedding): share provider-agnostic members and credential-URL rules fess#3202); that shared implementation also catches the protocol-relative (//user:pw@host) and scheme-less (user:pw@host) userinfo forms this repo's own scan missed.key=...query parameter form Gemini documents as an alternative to thex-goog-api-keyheader and RFC 3986 userinfo in the authority.api.urlwhose authority embeds userinfo is refused before a request URI is built from it; such a URL can never issue a request anyway, since the HTTP client rejects it per RFC 9110 section 4.2.4. The availability check fails closed and latches one error naming the setting and the supported alternatives, because it runs frominit()as a container post-construct and an escaping exception would abort assembly. The chat, stream, and embed entry points throw the same message instead.api.urlthat carries one still addresses/modelsand:batchEmbedContentscorrectly.Documentation
README.mdandCLAUDE.mddescribe thecontent_chunker.embedding.gemini.*keys with their correct configuration channel, scope the "no restart required" claim to the properties that actually honor it (timeoutandavailability.check.intervalare read once at initialization), and note the token-quota trap behind the 100-request batch cap:MAX_BATCH_SIZEbounds requests per call, not tokens, so a full sub-batch of large chunks can exceed a project's tokens-per-minute quota and fail non-transiently. Loweringcontent_chunker.length.chunk_sizeis the fix.Test plan
mvn test— 208 tests, 0 failures, run against snapshot15.8.0-20260802.120125-72, the first core snapshot published after codelibs/fess#3202 merged:GeminiEmbeddingClientTestGeminiEmbeddingClientConfigChannelTestsystemPropertiesinjection seamGeminiLlmClientTestGeminiLlmClientRetryTest