Skip to content

fix: param binding in search_similar_chunks with filters (regression from #655)#16

Open
dklymentiev wants to merge 2 commits into
mainfrom
fix/search-filter-param-binding
Open

fix: param binding in search_similar_chunks with filters (regression from #655)#16
dklymentiev wants to merge 2 commits into
mainfrom
fix/search-filter-param-binding

Conversation

@dklymentiev

Copy link
Copy Markdown
Owner

Summary

POST /search returns 500 Internal Server Error on every request that carries any of tags, date_from, or date_to. The failure comes from the workspace-scoped filter branch of search_similar_chunks in mesh/crud.py, which binds the outer LIMIT integer to the workspace_id slot (and vice versa).

This is a regression introduced by PR #655 (chunk vector search broken with workspace RLS): that PR added a workspace predicate inside the dynamic filter branch but left params.append(limit) above the workspace append, shifting every subsequent $N slot by one.

Root cause

The filter branch builds parameters through a running idx. With, say, only date_from set, the runtime state ends up as:

$N intended actually
$1 query_vector query_vector
$2 similarity_thr similarity_thr
$3 chunk_limit chunk_limit
$4 date_from date_from
$5 workspace_id limit (int)
$6 outer LIMIT ws (str)

asyncpg raises on $5:

ERROR:mesh.crud:Failed to search similar chunks:
  invalid input for query argument $5: 30 (expected str, got int)

The no-filter else branch is unaffected because it pins positions 1..5 literally and does not go through the dynamic idx builder.

Fix

Append limit after the workspace parameter and reference it via an explicit outer_limit_ref so the textual query and *params stay in sync. Traced through for all 7 filter combinations (tags only, date_from only, date_to only, and every pair/triple) — bindings align in every case.

Reproduction

Before this PR:

curl -X POST http://localhost:8000/search \
    -H "X-Workspace: any-workspace" \
    -H "Content-Type: application/json" \
    -d '{"query":"python","date_from":"2026-01-01T00:00:00"}'
# {"detail":"Internal server error"}  →  500

After this PR: same request returns 200 with documents correctly scoped to any-workspace and filtered by the created_at predicate.

Blast radius

  • Files: 1 (mesh/crud.py)
  • Functions: 1 (search_similar_chunks)
  • Branches: filter branch only; no-filter branch untouched
  • Lines: +11 / -2
  • SQL semantics: unchanged (same query text, same plan)
  • Schema / RLS / auth / workspaces: untouched
  • API contract: unchanged

Any caller that was previously getting a 500 from a filtered search (e.g. scout-matcher, HQ memory search with tags, MCP memory_search with tags) will start getting correct results. No caller sees a behavior downgrade — the filter branch was 100% broken before this change.

Tests

Existing test suite mocks crud.EmbeddingCRUD at the class level (tests/conftest.py), so the real SQL path is not exercised and the regression slipped through. A dedicated integration test for the three filter axes (tags, date_from, date_to) would have caught this — intentionally not added in this PR to keep it focused on the regression fix; happy to follow up with a test-only PR.

Verification performed

  • POST /search without filters: 200, unchanged results ✓
  • POST /search with date_from: 200 (was 500) ✓
  • POST /search with tags: 200 (was 500) ✓
  • POST /search with tags + date_from + date_to: 200 (was 500) ✓
  • End-to-end downstream consumer (scout-matcher running against freshness window): SUCCESS duration=110.48s (was FAILED error=500 on first subscriber) ✓
  • Tracing all 7 filter combinations by hand ✓

Out of scope

  • Adding integration test for filter branches
  • Updating CHANGELOG.md
  • The #655 workaround for HNSW + RLS interaction is preserved as-is

🤖 Generated with Claude Code

dklymentiev and others added 2 commits March 27, 2026 07:57
LLM agents sometimes pass tags as comma-separated string instead of
array. All tag parameters now accept both formats via _parse_tags()
helper. Fully backward-compatible -- arrays work as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…from #655)

When any of tags/date_from/date_to was set in SearchRequest, the filter
branch of search_similar_chunks bound the LIMIT integer to the
workspace_id slot (and vice versa), causing:

    invalid input for query argument $N: 30 (expected str, got int)

and a 500 on every /search that carried a filter.

Root cause: #655 added a workspace filter inside the inner WHERE of the
filter branch, but left `params.append(limit)` above the workspace
append. Because this branch labels parameters through a running `idx`,
the early `limit` push shifted every subsequent slot by one:

    $idx  intended         actually
    ----  ----------------  ----------------
    $5    c.workspace_id    limit  (int)   <- 500: expected str
    $6    outer LIMIT       ws     (str)

Fix: append `limit` after the workspace parameter and reference it via
an explicit `outer_limit_ref` so the textual query and *params stay in
sync. No change to the no-filter branch (it pins positions 1..5
literally and is unaffected).

Reproduce before fix:
    curl -X POST http://localhost:8000/search \
        -H "X-Workspace: any-ws" \
        -d '{"query":"x","date_from":"2026-01-01T00:00:00"}'
    # 500 Internal Server Error

After fix: 200 with results scoped to the requested workspace.

Existing tests mock crud.EmbeddingCRUD at the class level
(tests/conftest.py), so the real SQL path was not covered; an
integration test for the filter branches is left to a follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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