Skip to content

feat: semantic search and similar feedback API#40

Merged
xernobyl merged 51 commits into
mainfrom
feat/semantic-search
Mar 4, 2026
Merged

feat: semantic search and similar feedback API#40
xernobyl merged 51 commits into
mainfrom
feat/semantic-search

Conversation

@xernobyl
Copy link
Copy Markdown
Contributor

@xernobyl xernobyl commented Feb 25, 2026

Adds semantic search and similar feedback endpoints, and improves embedding configuration and robustness.

Semantic search & similar feedback

  • POST /v1/feedback-records/search/semantic – embeds the query and returns feedback records with similarity scores (cosine, 0..1)
  • GET /v1/feedback-records/{id}/similar – returns feedback records similar to a given one by embedding

Both endpoints require embeddings to be configured (EMBEDDING_PROVIDER + EMBEDDING_MODEL). When disabled, they return 503 Service Unavailable. Features:

  • Query embedding cache (LRU) for repeated searches
  • Keyset (cursor-based) pagination for stable, efficient paging
  • hasMore flag so pagination works correctly when the repo over-fetches and filters by minScore

Embedding configuration changes

  • EMBEDDING_PROVIDER and EMBEDDING_MODEL both required to enable embeddings; no default model
  • EMBEDDING_PROVIDER_API_KEY required for openai and google; server fails fast if missing
  • Provider name normalized to lowercase
  • EMBEDDING_NORMALIZE env var for L2-normalizing vectors before storage
  • EmbeddingPrefixForProvider future proofing support for other embedding providers

Backfill-embeddings

  • Creates embedding client directly (openai/google) with same options as API
  • Uses FeedbackEmbeddingWorker for job processing
  • Requires .env; fails fast if missing
  • make run-backfill-embeddings target

Other changes

  • JSON decode errors: response.JSONDecodeErrorDetail for clearer 400 messages on invalid request bodies
  • OpenAPI: realistic examples for parameters; maximum: 1000 for search offset; search endpoints documented
  • CI: Schemathesis split – main step excludes search (503); separate step tests search endpoints with relaxed checks
  • Migration 005: CREATE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY for idx_embeddings_model (no write blocking)
  • Normalize: precompute invMag and multiply instead of divide in L2 normalization loop

How should this be tested?

Unit tests

make test-unit

Integration tests

make dev-setup   # or: docker-up, init-db
make tests

Semantic search (requires embeddings)

  1. Configure .env:
    EMBEDDING_PROVIDER=openai
    EMBEDDING_MODEL=text-embedding-3-small
    EMBEDDING_PROVIDER_API_KEY=sk-...
    
  2. Start API: make run
  3. Backfill embeddings: make run-backfill-embeddings
  4. Semantic search:
    curl -X POST "http://localhost:8080/v1/feedback-records/search/semantic" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"query":"login is slow","tenant_id":"org-123"}'

API contract tests

make schemathesis   # with API running; runs search endpoints when embeddings enabled

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Repository Guidelines
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand bits
  • Ran make build
  • Ran make tests (integration tests)
  • Ran make fmt and make lint; no new warnings
  • Removed debug prints / temporary logging
  • Merged the latest changes from main onto my branch
  • If database schema changed: migration 005 uses CONCURRENTLY; ran make migrate-validate

Appreciated

  • If API changed: OpenAPI spec updated; contract tests in CI
  • Updated docs if necessary
  • Ran make tests-coverage for meaningful logic changes

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 25, 2026

✱ Stainless preview builds

This PR will update the hub SDKs with the following commit message.

feat: semantic search and similar feedback API
hub-openapi studio · code

Your SDK built successfully.
generate ✅

hub-typescript studio · code

Your SDK built successfully.
generate ✅build ✅lint ✅test ✅


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-03-04 11:04:02 UTC

@xernobyl xernobyl marked this pull request as ready for review February 26, 2026 16:06
@xernobyl xernobyl marked this pull request as draft February 26, 2026 16:06
Base automatically changed from feat/embeddings to main February 27, 2026 09:51
Copy link
Copy Markdown
Member

@mattinannt mattinannt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xernobyl Thanks for the PR :-)

Looking good already; we just need some fine tuning :-)

I'll also test the functionality in more detail in the coming days and also build a small use-case to test this with real data.

Comment thread internal/service/embedding_job_args.go
Comment thread openapi.yaml
Comment thread openapi.yaml
Comment thread cmd/backfill-embeddings/main.go Outdated
Comment thread internal/service/search_service.go Outdated
Comment thread internal/repository/embeddings_repository.go Outdated
Comment thread openapi.yaml
@xernobyl xernobyl requested a review from mattinannt March 4, 2026 09:10
@mattinannt
Copy link
Copy Markdown
Member

@coderabbitai please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

@mattinannt Sure! I'll kick off a review of this PR right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

@mattinannt: I'll kick off a full review of this PR right away! This is a substantial feature addition covering semantic search, similar feedback endpoints, embedding provider abstraction, caching, observability, and migrations. I'll examine all the changes thoroughly.

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Member

@mattinannt mattinannt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes; works great! :-)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 53-54: Add and expose the SEARCH_RATE_LIMIT_REQUESTS_PER_MINUTE
setting in the runtime config by adding a SearchRateLimitRequestsPerMinute int
field to the Config struct in internal/config/config.go and loading it from the
environment in the config loader (e.g., where other env vars are parsed in
LoadConfig/NewConfig). Implement a rate-limiting middleware in
internal/api/middleware (or extend existing middleware) that reads the
configured limit and enforces per-API-key per-minute limits for the semantic
search and similar-feedback routes, then register/apply that middleware in
cmd/api/app.go for the relevant handlers (semantic-search and similar-feedback
endpoints) so the value from Config is actually enforced at runtime. Ensure the
middleware identifies requests by API key and returns the proper HTTP 429 when
the limit is exceeded.

In @.github/workflows/api-contract-tests.yml:
- Around line 122-128: The current CI workflow excludes the two search operation
IDs (semantic-search-feedback-records and similar-feedback-records) from the
generic Schemathesis step, dropping contract coverage; keep those
--exclude-operation-id entries if needed for the generic step but add a new
dedicated Schemathesis job/step that targets only these operations (use
--operation-id semantic-search-feedback-records --operation-id
similar-feedback-records or equivalent scoping) and do not pass the
not_a_server_error override so the documented 503 behavior is validated; ensure
the new step uses the same auth/header args as the others (e.g., -H
"Authorization: Bearer test-api-key-for-ci") and runs in CI alongside the
existing step.

In `@internal/api/handlers/search_handler.go`:
- Around line 253-269: The parseMinScore function currently treats a parse error
as 0, which silently disables the min_score filter; change parseMinScore so that
when s is empty or when strconv.ParseFloat(s, 64) returns an error it returns
defaultMinScore instead of 0, and keep the existing guards that clamp negative
values to 0 and cap the value at 1 (i.e., after successful parse, if val < 0
return 0, else return math.Min(val, 1)). This ensures malformed min_score falls
back to defaultMinScore while valid numeric inputs are still clamped to [0,1].

In `@internal/api/response/response.go`:
- Around line 89-95: The current logic in fieldNameForAPI that adds underscores
uses only the next-rune check, causing "TenantID" -> "tenanti_d"; update the
underscore condition to also detect a lower-to-upper transition by checking the
previous rune: insert '_' when i>0 && unicode.IsUpper(r) &&
(unicode.IsLower(rune(fieldPath[i-1])) || (i+1 < len(fieldPath) &&
unicode.IsLower(rune(fieldPath[i+1])))). Apply this change in the loop that
iterates over fieldPath so acronyms like "ID" become "id" and "TenantID" becomes
"tenant_id".

In `@internal/models/feedback_records.go`:
- Line 105: The comment on the SubmissionID field in the FeedbackRecord struct
contradicts the new mandatory/non-null invariant; update the comment on
SubmissionID (the struct tag `SubmissionID    string         
`json:"submission_id"``) to remove the parenthetical “DB NULL is exposed as ""”
or reword it to reflect that the field is non-null/mandatory (e.g., "mandatory;
never null"). Ensure the new comment accurately describes the enforced invariant
and remove any mention of DB NULL exposure.

In `@internal/service/search_service.go`:
- Around line 134-144: The pagination cursor is being suppressed by the
condition len(results) == limit even though the repo over-fetches and then
filters by minScore; update the logic that sets out.NextCursor (where
EncodeSearchCursor is called) to consider whether the repository fetched the
full fetchLimit (e.g., have the repo return a boolean like fetchedFullLimit or
similar alongside results) and set NextCursor when fetchedFullLimit is true or
when len(results) == limit (or alternatively set a cursor whenever len(results)
> 0); modify the service code that assigns out.Results and computes
out.NextCursor to use that new fetchedFullLimit flag instead of relying solely
on len(results) == limit so pages aren’t prematurely terminated.

In `@Makefile`:
- Line 1: The .PHONY line needs to include aliases "all" and "test" so
checkmake/lint won't flag missing required phony targets; update the .PHONY
declaration (which currently lists targets like help tests tests-coverage build
...) to also include the names all and test (as aliases that map to your
existing default targets) so CI/lint expectations are satisfied while preserving
current target behavior.
- Around line 66-69: The Makefile target run-backfill-embeddings should fail
fast if loading .env fails: update the recipe so that sourcing .env is required
(do not use a trailing semicolon that allows continuation), and ensure the shell
exits with a non-zero status when .env is missing or the source command fails
before invoking go run ./cmd/backfill-embeddings; modify the
run-backfill-embeddings recipe to check and source .env with a failing path (or
use &&/|| control flow) and only call go run when the .env load succeeded.

In `@migrations/005_embeddings_indexes_and_feedback_tenant_required.sql`:
- Line 4: The CREATE INDEX for idx_embeddings_model on the embeddings table is
not using CONCURRENTLY and will block writes on large tables; change the
statement that creates idx_embeddings_model to use "CREATE INDEX CONCURRENTLY
idx_embeddings_model ON embeddings (model)" so the B-tree build does not lock
writes, and ensure the migration runner supports concurrent index creation (or
run it outside a transaction if required).
- Line 25: The Down migration currently drops idx_embeddings_model with a
regular DROP INDEX; change it to use DROP INDEX CONCURRENTLY IF EXISTS
idx_embeddings_model to match the other index (idx_embeddings) and avoid
blocking writes during rollback, and ensure this CONCURRENTLY call is executed
outside of a transaction if your migration runner requires that.

In `@openapi.yaml`:
- Around line 576-580: The OpenAPI parameter schema for the offset parameter
currently documents a 1000 cap in the description but does not enforce it;
update the offset parameter schemas (the entries where description reads "Number
of results to skip (OFFSET-based paging). Ignored if cursor is set. Capped at
1000 for performance.") to include "maximum: 1000" under the schema object (and
leave minimum: 0 and default: 0 intact) so the cap is enforced in both
occurrences.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 00aa247e-4680-4f05-9e2e-fcbc1e87e5ab

📥 Commits

Reviewing files that changed from the base of the PR and between a024ad0 and 9f203b7.

📒 Files selected for processing (18)
  • .env.example
  • .github/workflows/api-contract-tests.yml
  • Makefile
  • internal/api/handlers/feedback_records_handler.go
  • internal/api/handlers/search_handler.go
  • internal/api/handlers/search_handler_test.go
  • internal/api/handlers/webhooks_handler.go
  • internal/api/response/response.go
  • internal/api/response/response_test.go
  • internal/models/feedback_records.go
  • internal/repository/embeddings_repository.go
  • internal/service/feedback_records_service.go
  • internal/service/search_cursor.go
  • internal/service/search_service.go
  • internal/service/search_service_test.go
  • migrations/005_embeddings_indexes_and_feedback_tenant_required.sql
  • openapi.yaml
  • tests/integration_test.go

Comment thread .env.example Outdated
Comment thread .github/workflows/api-contract-tests.yml
Comment thread internal/api/handlers/search_handler.go
Comment thread internal/api/response/response.go Outdated
Comment thread internal/models/feedback_records.go Outdated
Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread migrations/005_embeddings_indexes_and_feedback_tenant_required.sql Outdated
Comment thread migrations/005_embeddings_indexes_and_feedback_tenant_required.sql Outdated
Comment thread openapi.yaml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (6)
internal/api/response/response.go (1)

89-95: ⚠️ Potential issue | 🟡 Minor

Acronym conversion is still incorrect for API field names.

On Line 90, the underscore rule still splits "TenantID" as "tenanti_d" instead of "tenant_id", which can mislead clients in type-error responses.

💡 Suggested fix
-	var buf strings.Builder
-
-	for i, r := range fieldPath {
-		if i > 0 && unicode.IsUpper(r) && (i+1 >= len(fieldPath) || unicode.IsLower(rune(fieldPath[i+1]))) {
-			buf.WriteByte('_')
-		}
-
-		buf.WriteRune(unicode.ToLower(r))
-	}
+	var buf strings.Builder
+	runes := []rune(fieldPath)
+
+	for i, r := range runes {
+		if i > 0 && unicode.IsUpper(r) {
+			prev := runes[i-1]
+			nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
+			if unicode.IsLower(prev) || (unicode.IsUpper(prev) && nextIsLower) {
+				buf.WriteByte('_')
+			}
+		}
+		buf.WriteRune(unicode.ToLower(r))
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/response/response.go` around lines 89 - 95, The
camelCase→snake_case conversion in the loop that builds buf mishandles acronyms
(e.g., "TenantID"); change the underscore insertion condition in that loop to
insert an underscore when the current rune is uppercase and either the previous
rune is lowercase OR the next rune is lowercase (i.e., if i>0 &&
unicode.IsUpper(r) && (unicode.IsLower(rune(fieldPath[i-1])) || (i+1<len &&
unicode.IsLower(rune(fieldPath[i+1]))))). Update the logic around the loop that
writes runes (the code handling fieldPath and buf) to use this condition so
acronyms like "ID" become "id" and "TenantID" becomes "tenant_id".
Makefile (2)

1-1: ⚠️ Potential issue | 🟡 Minor

Add required phony aliases all and test.

Line 1 still misses the aliases required by checkmake.

Suggested patch
-.PHONY: help tests tests-coverage build build-backfill-embeddings run run-backfill-embeddings init-db clean docker-up docker-down docker-clean deps install-tools fmt lint lint-new dev-setup test-all test-unit schemathesis install-hooks migrate-status migrate-validate river-migrate
+.PHONY: all test help tests tests-coverage build build-backfill-embeddings run run-backfill-embeddings init-db clean docker-up docker-down docker-clean deps install-tools fmt lint lint-new dev-setup test-all test-unit schemathesis install-hooks migrate-status migrate-validate river-migrate
+
+all: build
+test: test-all
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` at line 1, The .PHONY declaration is missing the required phony
aliases "all" and "test" required by checkmake; update the Makefile by adding
"all" and "test" to the .PHONY list (or create phony targets named all and test
that delegate to existing targets like build and tests) so checkmake recognizes
those aliases; modify the .PHONY line that currently lists targets to include
all and test (or add standalone phony target definitions for all and test that
run build/tests respectively).

66-69: ⚠️ Potential issue | 🟠 Major

run-backfill-embeddings should fail fast when env sourcing fails.

On Line 68, go run is executed after ;, so sourcing failures can be masked and produce misleading follow-on errors.

Suggested patch
 run-backfill-embeddings:
-	@(set -a && [ -f .env ] && . ./.env && set +a; go run ./cmd/backfill-embeddings)
+	`@set` -e; \
+	if [ -f .env ]; then set -a; . ./.env; set +a; fi; \
+	go run ./cmd/backfill-embeddings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 66 - 69, The Makefile target run-backfill-embeddings
can mask env sourcing failures because the go run is executed after a semicolon;
change the command so go run only runs if .env is found and successfully
sourced. Update the recipe for run-backfill-embeddings (the shell line that
currently uses set -a && [ -f .env ] && . ./.env && set +a; go run
./cmd/backfill-embeddings) to use && before go run (e.g., wrap the sourcing in
the same && chain or add an explicit failure when .env is missing) so sourcing
failures cause the make target to fail fast and prevent misleading follow-on
errors.
openapi.yaml (1)

576-580: ⚠️ Potential issue | 🟡 Minor

Enforce the documented offset cap in both search endpoint schemas.

Both descriptions say offset is capped at 1000, but the schema does not enforce it.

Proposed fix
                 - name: offset
                   in: query
                   description: Number of results to skip (OFFSET-based paging). Ignored if cursor is set. Capped at 1000 for performance.
                   schema:
                     type: integer
                     minimum: 0
+                    maximum: 1000
                     default: 0
@@
                 - name: offset
                   in: query
                   description: Number of results to skip (OFFSET-based paging). Ignored if cursor is set. Capped at 1000 for performance.
                   schema:
                     type: integer
                     minimum: 0
+                    maximum: 1000
                     default: 0

Also applies to: 668-672

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 576 - 580, The OpenAPI schemas declare that the
query parameter named "offset" is capped at 1000 in the description but the
schema lacks enforcement; update the two "offset" parameter schemas (the integer
schema blocks for the search endpoints) to include "maximum: 1000" so the
OpenAPI validation enforces the documented cap while keeping "minimum: 0" and
"default: 0" unchanged.
internal/models/feedback_records.go (1)

105-105: ⚠️ Potential issue | 🟡 Minor

Fix SubmissionID comment to match the non-null invariant.

The inline note still mentions DB NULL exposure, which conflicts with the current mandatory/non-null contract.

Proposed fix
-	SubmissionID    string          `json:"submission_id"` // mandatory; never null (DB NULL is exposed as "")
+	SubmissionID    string          `json:"submission_id"` // mandatory; never null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/models/feedback_records.go` at line 105, The comment for the
SubmissionID field is outdated and mentions DB NULL exposure; update the inline
comment on the SubmissionID struct field to state that SubmissionID is mandatory
and never null (reflecting the non-null invariant) and remove the note about DB
NULL being exposed as an empty string; target the SubmissionID field declaration
in the feedback records struct (SubmissionID string `json:"submission_id"`) and
make the comment concise and accurate about non-null/must be present.
internal/api/handlers/search_handler.go (1)

253-266: ⚠️ Potential issue | 🟡 Minor

Malformed min_score should fall back to default, not 0.

Invalid numeric input currently drops filtering to 0, which broadens results unexpectedly.

Proposed fix
 func parseMinScore(s string) float64 {
 	if s == "" {
 		return defaultMinScore
 	}

 	val, err := strconv.ParseFloat(s, 64)
 	if err != nil {
-		return 0
+		return defaultMinScore
 	}

 	if val < 0 {
 		return 0
 	}

 	return math.Min(val, 1)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/handlers/search_handler.go` around lines 253 - 266, The
parseMinScore function currently returns 0 on parse errors or out-of-range
values; change it to return defaultMinScore instead when input is malformed. In
parseMinScore, when strconv.ParseFloat returns an error or the parsed val is
outside [0,1] (val < 0 or val > 1), return defaultMinScore; only return the
parsed val when it is within the valid range. Ensure you reference the existing
parseMinScore function and defaultMinScore constant when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/config/config.go`:
- Around line 99-100: The doc comment for the exported function GetEnvAsBool is
incorrect: it claims unrecognized values map to false but the implementation
returns the provided defaultValue; update the comment to accurately describe
behavior (accepted true values "true", "1", "yes" case-insensitive => true;
accepted false values "false", "0", "no" => false; any other or missing value =>
returns the supplied defaultValue) so callers are not misled; reference the
GetEnvAsBool function in the comment and keep wording concise.

In `@internal/service/embedding_provider_test.go`:
- Around line 196-211: Add tests to TestEmbeddingPrefixForProvider that assert
EmbeddingPrefixForProvider returns "passage: " for the full E5 model identifiers
"intfloat/multilingual-e5-large" and "intfloat/e5-large" (and their uppercase
variants if you want case-insensitivity coverage). Update the
TestEmbeddingPrefixForProvider function to include t.Run subtests calling
EmbeddingPrefixForProvider with those exact strings and using assert.Equal(t,
"passage: ", ...) to verify behavior alongside the existing "e5" case.

In `@internal/service/embedding_provider.go`:
- Around line 196-210: Update the EmbeddingPrefixForProvider function: change
the "mistral" case to return an empty string (remove "search_document: ") and
adjust the function comment to no longer list Mistral as requiring a prefix;
instead document that Mistral uses the API parameter input_type to distinguish
query vs document. Also explicitly list the supported E5 model variants
("intfloat/multilingual-e5-large" and "intfloat/e5-large") in the comment and
keep their case returning "passage: ". Reference: function
EmbeddingPrefixForProvider and the switch cases for "mistral" and the E5
variants.

In `@internal/service/search_service.go`:
- Around line 135-137: The code that generates a next-cursor by accessing last
:= results[len(results)-1] must be guarded against non-positive limits to avoid
a panic when limit == 0; update the logic around the next-cursor generation (the
blocks that check if len(results) == limit and then take
results[len(results)-1], including the second occurrence around lines 197-199)
to first ensure limit > 0 and len(results) > 0 before indexing, and only compute
the last element/next-cursor when those conditions hold.
- Around line 23-25: Update the sentinel error ErrMissingTenantID so its text
uses the service/API terminology "tenant_id" instead of "environment_id"; locate
the ErrMissingTenantID declaration in internal/service/search_service.go and
change the error message string to something like "tenant_id is required" to
keep naming consistent across the codebase.

In `@pkg/embeddings/normalize.go`:
- Around line 24-29: The normalization loop currently divides each element by
magnitude per iteration; precompute the inverse once and multiply instead to
improve performance: compute invMag := 1.0 / magnitude (as float64), then in the
loop replace vector[i] = float32(float64(vector[i]) / magnitude) with vector[i]
= float32(float64(vector[i]) * invMag). Update references in the normalization
function where magnitude and vector are used to use invMag in the for loop.

---

Duplicate comments:
In `@internal/api/handlers/search_handler.go`:
- Around line 253-266: The parseMinScore function currently returns 0 on parse
errors or out-of-range values; change it to return defaultMinScore instead when
input is malformed. In parseMinScore, when strconv.ParseFloat returns an error
or the parsed val is outside [0,1] (val < 0 or val > 1), return defaultMinScore;
only return the parsed val when it is within the valid range. Ensure you
reference the existing parseMinScore function and defaultMinScore constant when
making the change.

In `@internal/api/response/response.go`:
- Around line 89-95: The camelCase→snake_case conversion in the loop that builds
buf mishandles acronyms (e.g., "TenantID"); change the underscore insertion
condition in that loop to insert an underscore when the current rune is
uppercase and either the previous rune is lowercase OR the next rune is
lowercase (i.e., if i>0 && unicode.IsUpper(r) &&
(unicode.IsLower(rune(fieldPath[i-1])) || (i+1<len &&
unicode.IsLower(rune(fieldPath[i+1]))))). Update the logic around the loop that
writes runes (the code handling fieldPath and buf) to use this condition so
acronyms like "ID" become "id" and "TenantID" becomes "tenant_id".

In `@internal/models/feedback_records.go`:
- Line 105: The comment for the SubmissionID field is outdated and mentions DB
NULL exposure; update the inline comment on the SubmissionID struct field to
state that SubmissionID is mandatory and never null (reflecting the non-null
invariant) and remove the note about DB NULL being exposed as an empty string;
target the SubmissionID field declaration in the feedback records struct
(SubmissionID string `json:"submission_id"`) and make the comment concise and
accurate about non-null/must be present.

In `@Makefile`:
- Line 1: The .PHONY declaration is missing the required phony aliases "all" and
"test" required by checkmake; update the Makefile by adding "all" and "test" to
the .PHONY list (or create phony targets named all and test that delegate to
existing targets like build and tests) so checkmake recognizes those aliases;
modify the .PHONY line that currently lists targets to include all and test (or
add standalone phony target definitions for all and test that run build/tests
respectively).
- Around line 66-69: The Makefile target run-backfill-embeddings can mask env
sourcing failures because the go run is executed after a semicolon; change the
command so go run only runs if .env is found and successfully sourced. Update
the recipe for run-backfill-embeddings (the shell line that currently uses set
-a && [ -f .env ] && . ./.env && set +a; go run ./cmd/backfill-embeddings) to
use && before go run (e.g., wrap the sourcing in the same && chain or add an
explicit failure when .env is missing) so sourcing failures cause the make
target to fail fast and prevent misleading follow-on errors.

In `@openapi.yaml`:
- Around line 576-580: The OpenAPI schemas declare that the query parameter
named "offset" is capped at 1000 in the description but the schema lacks
enforcement; update the two "offset" parameter schemas (the integer schema
blocks for the search endpoints) to include "maximum: 1000" so the OpenAPI
validation enforces the documented cap while keeping "minimum: 0" and "default:
0" unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57776643-c1e8-4690-8bc7-22ee679f7afb

📥 Commits

Reviewing files that changed from the base of the PR and between 115f6b9 and 9f203b7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (34)
  • .env.example
  • .github/workflows/api-contract-tests.yml
  • Makefile
  • cmd/api/app.go
  • cmd/backfill-embeddings/main.go
  • go.mod
  • internal/api/handlers/feedback_records_handler.go
  • internal/api/handlers/search_handler.go
  • internal/api/handlers/search_handler_test.go
  • internal/api/handlers/webhooks_handler.go
  • internal/api/response/response.go
  • internal/api/response/response_test.go
  • internal/config/config.go
  • internal/googleai/client.go
  • internal/models/embeddings.go
  • internal/models/feedback_records.go
  • internal/observability/aggregate.go
  • internal/observability/cache.go
  • internal/observability/names.go
  • internal/openai/client.go
  • internal/repository/embeddings_repository.go
  • internal/service/embedding_client.go
  • internal/service/embedding_provider.go
  • internal/service/embedding_provider_test.go
  • internal/service/feedback_records_service.go
  • internal/service/search_cursor.go
  • internal/service/search_service.go
  • internal/service/search_service_test.go
  • internal/workers/feedback_embedding.go
  • migrations/005_embeddings_indexes_and_feedback_tenant_required.sql
  • openapi.yaml
  • pkg/embeddings/normalize.go
  • pkg/embeddings/normalize_test.go
  • tests/integration_test.go

Comment thread internal/config/config.go
Comment thread internal/service/embedding_provider_test.go
Comment thread internal/service/embedding_provider.go
Comment thread internal/service/search_service.go Outdated
Comment thread internal/service/search_service.go Outdated
Comment thread pkg/embeddings/normalize.go
@xernobyl xernobyl enabled auto-merge March 4, 2026 10:50
@xernobyl xernobyl disabled auto-merge March 4, 2026 10:50
@xernobyl xernobyl enabled auto-merge March 4, 2026 10:58
@xernobyl xernobyl added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit eed1a56 Mar 4, 2026
9 checks passed
@xernobyl xernobyl deleted the feat/semantic-search branch March 4, 2026 11:02
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.

3 participants