Skip to content

feat(search): send fusion, granularity and min_relevance from the search page - #331

Open
cbcoutinho wants to merge 1 commit into
mainfrom
feat/search-setting-parity
Open

feat(search): send fusion, granularity and min_relevance from the search page#331
cbcoutinho wants to merge 1 commit into
mainfrom
feat/search-setting-parity

Conversation

@cbcoutinho

Copy link
Copy Markdown
Owner

The Semantic Search page could reach almost none of the settings the MCP server supports. An audit of both search paths — this page (POST /api/v1/vector-viz/search) and the Nextcloud global search bar (POST /api/v1/search) — found that of every parameter a retrieval benchmark varies, this page could request exactly one: limit, and only up to 50.

That makes measured retrieval improvements undeliverable to users, which is how it was found: a benchmark sweep produced clean results for settings nobody could actually request.

Three parameters accepted by the server and never sent

parameter what was wrong
fusion The admin's rrf/dbsf choice reached only the global search bar. It also selects which relevance curve the server applies, so omitting it pinned the reported relevance to the RRF-ordinal mapping regardless of configuration. Now sent, defaulting to the admin setting so both surfaces agree.
granularity One row per document rather than per passage — the shape "which files mention X" wants.
min_relevance The server-side relevance cut, applied before the page is trimmed to limit, so a filtered search still returns a full page.

The two relevance controls were indistinguishable

The existing "Minimum relevance" slider is not min_relevance. It filters client-side over rows already returned, so raising it shortens the list rather than reaching deeper into the corpus. Both controls now exist, are labelled distinctly, and each carries a line saying which it is:

  • Server relevance cut — "Applied by the server before the page is cut to the result limit, so a filtered search still returns a full page. Filters the search itself."
  • Hide below relevance — "Hides rows from the results already returned. Does not fetch more, so raising it shortens the list."

Also: a 422 was surfacing as a 500

A 422 from the MCP server means a well-formed request this deployment cannot serve — unsupported search algorithm, document granularity on a dense-only algorithm, reranking when it is not configured. It was being collapsed into RuntimeException("Unexpected HTTP 422 from MCP server") and returned as an opaque 500, which reads to the user as an outage and discards a structured, actionable body. The server's own error now reaches the client with a 422.

Same class of bug as the parity gaps themselves: a meaningful signal discarded in transit.

Verification

End to end against a live login-flow stack with 60 indexed Deck cards:

check result
granularity=document 200, results returned
granularity=document + algorithm=semantic 422 with granularity_unsupported_for_algorithm and the server's payload
fusion=rrf vs dbsf relevance_source flips fusion_ordinaluncalibrated
min_relevance=0.5 5-row page → 1 row
invalid values 400 with actionable messages on all three

The fusion check is the one worth noting: result ordering was identical on this corpus, but the relevance curve changed — so asserting that the parameter arrives rather than that results change is what proves it is wired.

Compatibility

fusion and min_relevance were already accepted by the server. granularity needs the matching server change (nextcloud-mcp-server #1359); an older server ignores the unknown field rather than erroring, so there is no deployment-ordering requirement in either direction.

Checks

npm run lint — 0 errors (one pre-existing vizPlot warning, untouched). npm run stylelint — clean. composer cs:check — 0 of 102 files need fixing. php -l clean on both modified PHP files.

Deck #1070.


This PR was generated with the help of AI, and reviewed by a Human

…rch page

The app's Semantic Search page could reach almost none of the settings the MCP
server supports. An audit of both search paths found that of every parameter
the retrieval benchmark varies, this page could request exactly one (`limit`,
and only up to 50) — so measured improvements were not deliverable to users.

Three parameters were accepted by the server and never sent from here:

- `fusion` — the admin's rrf/dbsf choice reached only the Nextcloud global
  search bar. It also selects which relevance curve the server applies, so
  omitting it pinned the reported relevance to the RRF-ordinal mapping
  regardless of configuration. Now sent, defaulting to the admin setting so
  both surfaces finally agree.
- `granularity` — one row per document rather than per passage, the shape
  "which files mention X" wants. Requires the matching server change; an older
  server ignores the field rather than erroring.
- `min_relevance` — the server-side relevance cut, applied BEFORE the page is
  trimmed to `limit`, so a filtered search still returns a full page.

The existing "Minimum relevance" slider is NOT that parameter: it filters
client-side over rows already returned, so raising it shortens the list rather
than reaching deeper. Both controls are now present, labelled distinctly, and
each says which it is — previously they were indistinguishable.

Also fixes a related swallowing bug: a 422 from the MCP server (a well-formed
request this deployment cannot serve — unsupported algorithm, document
granularity on a dense-only algorithm, reranking when unconfigured) was
collapsed into `RuntimeException("Unexpected HTTP 422")` and surfaced as an
opaque 500 that reads like an outage. The server's structured body now reaches
the client with a 422.

Verified end to end against a live login-flow stack: granularity=document
returns results, document+semantic returns the server's 422 payload, rrf vs
dbsf flips relevance_source between fusion_ordinal and uncalibrated (proving
the value reaches the server rather than being accepted and ignored), and
min_relevance=0.5 cuts a 5-row page to 1.

Deck #1070.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astrolabe-docs Ready Ready Preview Aug 19, 2026 8:50pm

@sonarqubecloud

Copy link
Copy Markdown

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown

Claude finished @cbcoutinho's task in 2m 33s —— View job


Review complete

  • Gather context (PR body, CLAUDE.md, diff)
  • Read changed files
  • Review lib/Controller/ApiController.php
  • Review lib/Service/McpServerClient.php
  • Review src/App.vue
  • Check test coverage against CLAUDE.md's mandatory API-surface testing gate
  • Post final review

Summary

Solid, well-documented parity fix — the fusion/granularity/min_relevance validation in ApiController::search() mirrors the existing pattern in that file (date-range and unsupported-algorithm checks), and the 422-passthrough in McpServerClient (both the streaming decode path and detectErrorResponse()) is a real bug fix, not just a nice-to-have. The two relevance sliders being disambiguated in the UI is a genuine usability fix.

Findings

1. No test coverage added — this is the mandatory gate in this repo's own CLAUDE.md

"Any PR that adds or changes API surface ... MUST ship, in the same PR, with: End-to-end coverage ... and Contract (Pact) coverage ... Reviewers (including the Claude review bot) must flag any such PR that lacks this coverage as a required change."

This PR touches #[NoAdminRequired] search() and vectorStatus() in lib/Controller/ApiController.php — new query params (rerank, fusion, granularity, min_relevance), new validation branches (three new 400 paths), a new 422-forwarding branch, and two new response fields (reranked, rerank_available) — but git diff origin/main...HEAD --stat -- tests/ is empty; no test file was touched.

This isn't a case of missing infrastructure: tests/unit/Controller/ApiControllerSearchTest.php already exists in this exact area and already tests the identical shape of change this PR makes (testRejectsInvertedDateRangeWith400, testRejectsUnsupportedAlgorithmWith422). Extending it with testRejectsInvalidFusionWith400, testRejectsInvalidGranularityWith400, testRejectsMinRelevanceOutOfRangeWith400, and a test asserting the generic MCP-422 passthrough (unsupported key stripped, body forwarded, status 422) would be low-effort and directly follow the file's existing conventions. The PR body's manual verification against a live stack is valuable but doesn't substitute for this per the gate's own wording.

If the intent is to treat this as covered by the "known gaps" list (provider-state endpoint not implemented), that should be stated explicitly in the PR description per CLAUDE.md's instructions — right now it's silent, which is the thing the gate is meant to prevent.

2. The rerank feature isn't mentioned anywhere in the PR description

The diff adds a complete cross-encoder rerank feature end-to-end: a rerank request param (lib/Controller/ApiController.php:232, lib/Service/McpServerClient.php:401), a rerank_available capability flag surfaced from vectorStatus() (lib/Controller/ApiController.php:487-505), a reranked response field, and a new UI toggle + hint text in src/App.vue (~line 117-127, 1207-1218). None of this is mentioned in the PR title, description, or the "Verification" table, which only covers fusion/granularity/min_relevance/422. Worth either splitting into its own PR with its own description and verification steps, or updating this description so it actually reflects the full diff — as written it undersells the scope of what's shipping and what should be reviewed/tested.

3. Minor — vectorStatus() now makes two sequential upstream calls

lib/Controller/ApiController.php:497 adds an unconditional $this->client->getStatus() call alongside the existing getVectorSyncStatus(), purely to read one boolean (rerank_available). Today this is only called on mount and manual refresh (no polling found in App.vue), so the added latency/load is small, but it doubles the MCP-server round trips for this endpoint. Not blocking — just flagging in case this endpoint is later put on a polling interval.

Nits

  • lib/Controller/ApiController.php:375: strtolower(trim($granularity)) ?: 'chunk' falls back to 'chunk' for any falsy string (e.g. a literal "0"), not just empty — harmless given the real value set, but === '' would be more precise and consistent with the $fusion check three lines above.

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