Skip to content

feat(embedding): add GeminiEmbeddingClient implementing the EmbeddingClient SPI - #21

Merged
marevol merged 1 commit into
mainfrom
add-content-chunk-embedding-client
Aug 4, 2026
Merged

feat(embedding): add GeminiEmbeddingClient implementing the EmbeddingClient SPI#21
marevol merged 1 commit into
mainfrom
add-content-chunk-embedding-client

Conversation

@marevol

@marevol marevol commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds GeminiEmbeddingClient, the Gemini-backed implementation of the EmbeddingClient SPI introduced in Fess core by codelibs/fess#3184 (merged), 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 — 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 from init(), 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, no IOException retry, single-tier response timeout) rather than OllamaEmbeddingClient's conventions.

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.
  • 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 (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 (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.
  • 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 describe the content_chunker.embedding.gemini.* keys with their correct configuration channel, scope the "no restart required" claim to the properties that actually honor it (timeout and availability.check.interval are read once at initialization), and note the token-quota trap behind the 100-request batch cap: MAX_BATCH_SIZE bounds 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. Lowering content_chunker.length.chunk_size is the fix.

Test plan

mvn test208 tests, 0 failures, run against snapshot 15.8.0-20260802.120125-72, the first core snapshot published after codelibs/fess#3202 merged:

Suite Tests
GeminiEmbeddingClientTest 50
GeminiEmbeddingClientConfigChannelTest 6 — drives the real systemProperties injection seam
GeminiLlmClientTest 150 — pre-existing suite, plus new coverage for the shared URL masking and credential refusal
GeminiLlmClientRetryTest 2

@marevol
marevol force-pushed the add-content-chunk-embedding-client branch from b2209d3 to 9a8ea7f Compare August 2, 2026 02:17
@marevol marevol changed the title Add GeminiEmbeddingClient implementing Fess's new EmbeddingClient SPI feat(embedding): add GeminiEmbeddingClient implementing the EmbeddingClient SPI Aug 2, 2026
@marevol
marevol marked this pull request as ready for review August 2, 2026 02:19
…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
marevol force-pushed the add-content-chunk-embedding-client branch from d423d02 to 3461013 Compare August 2, 2026 12:35
@marevol
marevol merged commit abb762c into main Aug 4, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant