Conversation
Adds the read endpoint (step 4 of 4) for the Lookup Service topic dimension (LLMO-7445, M2 v1), built on the M1 by-url read patterns: - src/support/lookup-by-topic.js engine: parse topics[] (1-100, drop-don't-fail), resolve each query vector via the semantic_query_embedding cache (hit) or AzureEmbeddingClient batch embed + cache upsert (miss), ANN cosine search via lookupOpportunitiesByVector (site-scoped, dedup, floor, top-k), union + hydrate + site-ownership re-filter + status filter + project. - Controller getByTopic mirrors getByUrl (requirePostgrestClient -> 503, locale, log, site-ownership filter) + embedding-client construction (503 on misconfig) + LOOKUP_TOPIC_MIN_SCORE env. - Route POST /sites/:siteId/opportunities/by-topics; :siteId classified in facs-capabilities; opportunity:read in required-capabilities. - OpenAPI: LookupByTopicRequest, OpportunitiesByTopicResponse, path, examples. - Unit tests (engine + controller) and a gated IT under test/it/postgres. Ref: LLMO-7445
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Hey @tb-adbe,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Request changes - two must-fix items before this ships.
Complexity: HIGH - large diff (1670 lines, 20 files); API surface + FACS signal.
Changes: Adds a POST /sites/:siteId/opportunities/by-topics semantic lookup endpoint that embeds topic text, runs pgvector ANN searches, and returns ranked opportunities per topic with full FACS/access-control enforcement (20 files).
Note: CI checks are currently failing - resolve before merge.
Must fix before merge
- [Important] Missing length validation on
createEmbeddingsreturn value -src/support/lookup-by-topic.js:148(details inline) - [Important] Sequential PostgREST calls serialize latency across cache reads and ANN searches -
src/support/lookup-by-topic.js:133(details inline)
Non-blocking (4): minor issues and suggestions
- nit: Module doc says
POST .../opportunities/by-topic(singular) but the route isby-topics(plural) -src/support/lookup-by-topic.js:6 - suggestion: The
MAX_LOOKUP_MATCHESguard atsrc/support/lookup-by-topic.js:169fires after all ANN loops complete. Consider checkingallIds.lengthincrementally inside the per-topic loop and bailing early once the cap is reached, to avoid running all RPC calls before rejecting. - suggestion:
touchQueryEmbeddingawaits inside the for-loop but its.catch()already swallows errors - collect the promises and settle them after the loop rather than blocking each iteration -src/support/lookup-by-topic.js:140 - nit: The IT suite (
test/it/postgres/opportunities-by-topics.test.js) will fail CI until the data-service image is bumped to include the semantic migrations. Consider gating the describe block with abefore()hook that probes the table and callsthis.skip()if absent, so it does not block unrelated PRs.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 4m 16s | Cost: $6.89 | Commit: 289235b1b89043d5b71444665c48098e827d657b
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| */ | ||
| async function resolveTopicVectors(postgrestClient, embeddingClient, texts, log) { | ||
| const vectorByText = new Map(); | ||
| const misses = []; |
There was a problem hiding this comment.
issue (blocking): Missing length validation on createEmbeddings return value.
resolveTopicVectors calls embeddingClient.createEmbeddings(misses) and indexes into the result with vectors[i], assuming the returned array length matches misses.length. If the embedding service returns fewer vectors than inputs (partial failure, truncation, or a future SDK change), vectors[i] is undefined, which gets stored in the query cache and passed to the ANN RPC. This is a silent data corruption path - the cache would contain an undefined vector, and the RPC would either throw an opaque error or return garbage results.
Fix: After const vectors = await embeddingClient.createEmbeddings(misses), add a guard:
if (!Array.isArray(vectors) || vectors.length !== misses.length) {
throw new Error(`Embedding response length mismatch: expected ${misses.length}, got ${vectors?.length}`);
}This turns a silent corruption into a loud 5xx the caller can act on.
| if (src && Object.hasOwn(src, k)) { | ||
| out[k] = src[k]; | ||
| } | ||
| } |
There was a problem hiding this comment.
issue (blocking): Sequential PostgREST calls serialize latency across cache reads and ANN searches.
resolveTopicVectors loops sequentially with await getQueryEmbedding and await touchQueryEmbedding per text (lines 133-145), then the engine loops sequentially with await lookupOpportunitiesByVector per distinct topic (lines 235-248). With 100 distinct topics (the upper bound), this is up to 200+ sequential PostgREST round-trips before hydration even starts. At typical production latencies (5-10ms per call), 100 topics could add 1-2 seconds of pure serialization overhead.
The createEmbeddings(misses) call is already batched, which is the right shape. The cache reads and ANN searches should match.
Fix: Batch the cache reads with Promise.all(texts.map(text => getQueryEmbedding(...))). Run the touchQueryEmbedding calls as fire-and-forget with Promise.allSettled (they already swallow errors). Parallelize the ANN searches with Promise.all over the distinct texts after all vectors are resolved. If you need to limit concurrency at the upper bound, use a bounded concurrency pool rather than pure sequential.
Address PR review on the by-topics engine: - guard the createEmbeddings return length (loud error instead of an undefined vector cached + sent to the ANN RPC) - resolve cache reads and run ANN searches with bounded concurrency (pool 20) instead of sequentially; best-effort touch/upsert writes are fire-and-forget, settled after the loop - fix the module doc endpoint path (by-topic -> by-topics) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @MysticatBot — addressed in 85f48f0. Must-fix
Non-blocking Engine stays at 100% coverage. |
…7445) Thread the getQueryEmbedding hit's textHash into the best-effort access bump so data-access skips re-normalizing + re-hashing on the read path. Backward-compatible (the helper derives the hash when textHash is absent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @tb-adbe,
⚠️ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - well-structured addition that cleanly mirrors the established by-urls pattern with proper auth layering.
Complexity: HIGH - new API surface, FACS signal, 20 changed files across 1729 lines.
Changes: adds POST /sites/:siteId/opportunities/by-topics semantic lookup endpoint with pgvector ANN search, embedding resolution, bounded concurrency pool, durable query cache, full auth chain (site ownership + Summit-PLG + FACS composite), and comprehensive unit/IT coverage (20 files).
Note: Recommend a human read before merge - shared-contract change (docs/openapi/* files modified). The bot review is a complement to, not a replacement for, a human read here.
Note: CI checks are currently failing (it-postgres) - expected, pending data-service migration dependency.
Non-blocking (7): minor issues and suggestions
- nit:
parseLookupTopicssilently drops invalid entries without surfacing a count or warning to the caller - consistent with by-urls but worth noting for API consumers debugging mismatched result lengths -src/support/lookup-by-topic.js:93-95 - nit: OpenAPI
LookupByTopicRequest.minScore.default: 0.1is misleading for code-gen tools since the runtime default is env-configurable viaLOOKUP_TOPIC_MIN_SCORE- consider removing the schema-leveldefaultand keeping it in the description only -docs/openapi/schemas.yaml(LookupByTopicRequest) - nit: embedding client config error logged at
errorlevel when it is a known deployment configuration state (missing env var), not an unexpected failure -warnwould be more appropriate -src/controllers/opportunities.js:289 - suggestion:
projectLookupomitsforceFieldscompared to the by-url version'sprojectLookupResults- verify this is intentional (by-topic has no suggestion cross-join needingopportunityIdforce-include) -src/support/lookup-by-topic.js:149-166 - suggestion: add a focused unit test verifying
mapWithConcurrencyrespects its concurrency bound (max in-flight count) - the pool is tested indirectly through the engine but a direct test would catch regressions to unbounded fan-out -src/support/lookup-by-topic.js:64-76 - suggestion: add a controller-level assertion that
filterOpportunitiesByFacsCompositeandfilterForSummitPlgare wired into thefilterEntitiescallback - currently only the site-ownership re-filter is tested at the controller level, and the auth-chain steps are covered only by the IT suite -test/controllers/opportunities-by-topic.test.js - suggestion: consider testing locale passthrough to
OpportunityDto.toJSONat the controller level - the format validation is tested (400 on invalid) but the forwarding of a valid locale to the DTO serializer is not asserted -test/controllers/opportunities-by-topic.test.js
Previously flagged, now resolved
- Embedding length guard now validates
createEmbeddingsresponse length matches input - Sequential PostgREST calls replaced with bounded-concurrency
mapWithConcurrency(limit 20) - Module doc header corrected from
by-topictoby-topics touchQueryEmbeddingawait-in-loop folded into bounded writesMAX_LOOKUP_MATCHESearly-bail issue mooted by concurrency rewrite- Cache-hit access bump now passes
textHashto skip re-hashing (new commit bc1fde8)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 6m 30s | Cost: $13.31 | Commit: 85f48f04efa29f523158f985b1aa9e8daa5cf22d
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…e (LLMO-7445) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cText
parseLookupTopics now filters via the shared cleanTopicText helper (same junk
rejection as the audit-worker writer). The engine de-duplicates topics on the
normalized key instead of the raw string, so case/whitespace variants
("Invoicing" vs "invoicing") collapse to one embed + one ANN search while
every original input topic is still echoed in the response.
Ref: LLMO-7445
What
Read endpoint (step 4 of 4) for the Lookup Service topic dimension —
POST /sites/:siteId/opportunities/by-topicsreturns, per topic, the site's opportunities semantically related to it (LLMO-7445, M2 v1). Built on the M1by-urlread patterns.src/support/lookup-by-topic.jsengine (mirrors the by-url engine):{ topics[], k?, minScore?, status?, fields?, locale? }— topics 1–100, drop-don't-fail;kdefault 10 (max 100);minScoredefault0.1(LOOKUP_TOPIC_MIN_SCOREenv).semantic_query_embeddingcache hit (+ best-effort touch), else batchAzureEmbeddingClient.createEmbeddings(native 1536) + cache upsert.lookupOpportunitiesByVector— site-scoped ANN cosine RPC (dedupes to distinct opportunities, best score, floor, top-k).batchGetByKeys) → site-ownership re-filter + FACS/PLG → status-filter (hide IGNORED by default;statusoverrides) → project (lightweight default +fieldsopt-in).results[](one per input topic, ranked{ opportunityId, score }) + a singleopportunitiesid→DTO map.getByTopic— mirrorsgetByUrl(requirePostgrestClient→ 503, locale, log, site-ownership filter) plus embedding-client construction (503 on misconfig).POST /sites/:siteId/opportunities/by-topics(plural, matchingby-urls);:siteIdclassified infacs-capabilities.js;opportunity:readinrequired-capabilities.js.LookupByTopicRequest,OpportunitiesByTopicResponse, path + examples.Tests
test/support/lookup-by-topic.test.js) + isolated controller (test/controllers/opportunities-by-topic.test.js).test/it/postgres/opportunities-by-topics*) seedsopportunity_semantic_embedding+ a cache-hitsemantic_query_embeddingwith deterministic 1536-dim unit vectors (no real Azure call). Gated: the ITdocker-compose.ymldb→pgvector/pgvector:pg16(done) and themysticat-data-serviceimage must bump to the step-1 release before it passes in CI.Notes
AZURE_EMBEDDING_*(same deployment as the audit-worker).spacecat-shared(step 2, data-access + gpt-client) and the data-service schema/RPC (step 1) — bump both deps after those land.Related Issues