Skip to content

feat(knowledge): opt-in hybrid lexical + vector retrieval for KB search - #6124

Merged
waleedlatif1 merged 8 commits into
stagingfrom
kb-hybrid-retrieval
Jul 31, 2026
Merged

feat(knowledge): opt-in hybrid lexical + vector retrieval for KB search#6124
waleedlatif1 merged 8 commits into
stagingfrom
kb-hybrid-retrieval

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • KB search ranked purely on vector similarity, so exact tokens — error codes, ticket keys, SKUs, rare product names — retrieved poorly or not at all
  • Adds a full-text keyword leg over the already-present (but unused) embedding.content_tsv generated column and its GIN index, fused with the vector leg by reciprocal rank. No migration, no re-indexing
  • Opt-in: searchMode defaults to vector, so the block, v1 API, copilot, and guardrail RAG all keep their exact current ranking. searchMode: 'hybrid' turns it on, plus an advanced Retrieval Mode dropdown on the Knowledge block
  • Both search routes and the copilot knowledge tool now share one executeKnowledgeSearch dispatch instead of duplicating the retrieval branch
  • Documents searchMode in the v1 OpenAPI spec and the knowledge base guide

Type of Change

  • New feature

Testing

Verified against a local Postgres with a 20,003-chunk corpus:

  • searching PROJ-1234 returns nothing under vector, ranks 2 of 25 under hybrid
  • EXPLAIN ANALYZE confirms Bitmap Index Scan on emb_content_fts_idx (no seq scan)
  • the exact-token chunk disappears under every visibility filter (chunk disabled, doc disabled, still processing, user-excluded, archived, soft-deleted); cross-KB isolation and tag filters both hold on the keyword leg
  • quoted phrases, -negation, all-stopword queries, and hostile input (') OR 1=1 --) all behave correctly
  • 922 unit tests pass; every new assertion was mutation-checked to confirm it can fail

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

KB search ranked purely on pgvector cosine distance, which retrieves exact
tokens (error codes, ticket keys, identifiers, rare product names) poorly.

Add a full-text leg over the already-present generated `embedding.content_tsv`
column and its GIN index — no migration, no re-indexing — and fuse it with the
vector leg by reciprocal rank. Both legs run concurrently and share the same
visibility and tag-filter predicates; the lexical leg is best-effort and falls
back to vector-only on failure.

Hybrid is the default for every caller. `searchMode: 'vector'` on the internal
and v1 contracts (and an advanced Retrieval Mode dropdown on the Knowledge
block) restores the previous behavior.

Both search routes now share one `executeKnowledgeSearch` dispatch instead of
duplicating the three-branch retrieval logic.
Every existing caller — workflow block, v1 API, copilot, guardrail RAG — keeps
its current ranking. Hybrid retrieval is now requested explicitly via
`searchMode: 'hybrid'`.

Also routes the copilot knowledge tool through the shared
`executeKnowledgeSearch` dispatch so all four callers share one retrieval path,
and documents `searchMode` on the public v1 search endpoint in the OpenAPI spec.
Regenerates the knowledge integration reference for the new searchMode tool
param, and adds a Retrieval Mode section to the knowledge base workflow guide
explaining when hybrid beats vector-only.
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 31, 2026 7:32pm

Request Review

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes core KB retrieval paths and ranking for callers who opt into hybrid; default vector limits blast radius, but fusion and multi-leg SQL add complexity and new failure modes (partial fallback on keyword errors).

Overview
Adds opt-in hybrid retrieval for knowledge base search: searchMode defaults to vector (unchanged behavior); hybrid runs a Postgres full-text leg on the existing content_tsv GIN index and fuses it with vector results via reciprocal rank fusion.

Internal and v1 search routes, the workflow Knowledge block (advanced Retrieval Mode dropdown), the knowledge tool, and copilot query now route through a shared executeKnowledgeSearch instead of separate tag-only / vector-only / combined handlers. Hybrid mode runs executeKeywordSearch in parallel with the vector leg (rank-then-hydrate to avoid detoasting embeddings on every FTS match), merges with fuseByReciprocalRank (lexical list first on ties), and falls back to vector-only if the keyword leg errors.

Contracts, OpenAPI, and docs describe searchMode and when to use hybrid (exact codes, IDs, ticket keys).

Reviewed by Cursor Bugbot for commit f1a8e9d. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds opt-in hybrid knowledge retrieval while preserving vector search as the default.

  • Adds full-text keyword retrieval over the existing generated search-vector column and fuses lexical and vector rankings using reciprocal rank.
  • Centralizes retrieval dispatch across internal and v1 search routes and the copilot knowledge tool.
  • Exposes retrieval mode in the Knowledge block, API contracts, OpenAPI specification, and user documentation.
  • Adds coverage for lexical retrieval, fusion ordering, visibility filtering, routing, and backward-compatible defaults.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current fusion implementation resolves both previously reported tie-balancing defects and has direct regression coverage for them.

Important Files Changed

Filename Overview
apps/sim/app/api/knowledge/search/utils.ts Adds lexical retrieval, reciprocal-rank fusion with balanced tie handling, and the shared search dispatcher; the previously reported fusion defects are fixed at HEAD.
apps/sim/app/api/knowledge/search/route.ts Routes internal knowledge searches through the shared dispatcher while retaining embedding, filtering, reranking, and billing orchestration.
apps/sim/app/api/v1/knowledge/search/route.ts Adds retrieval-mode forwarding to the v1 endpoint through the shared search dispatcher.
apps/sim/app/api/knowledge/search/utils.test.ts Covers lexical search, hybrid dispatch, reciprocal-rank ordering, first-leg starvation, and shared-hit contribution accounting.
apps/sim/blocks/blocks/knowledge.ts Adds the advanced Retrieval Mode control to the Knowledge workflow block.
apps/docs/openapi.json Documents the optional vector-or-hybrid search mode for the public knowledge search API.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Caller[Knowledge search caller] --> Dispatch[executeKnowledgeSearch]
  Dispatch -->|vector| Vector[Vector retrieval]
  Dispatch -->|hybrid| Vector
  Dispatch -->|hybrid| Keyword[Full-text keyword retrieval]
  Vector --> Fusion[Reciprocal-rank fusion]
  Keyword --> Fusion
  Fusion --> Results[Top-K results]
  Vector -->|vector mode| Results
Loading

Reviews (7): Last reviewed commit: "perf(knowledge): stop the keyword leg de..." | Re-trigger Greptile

Comment thread apps/sim/app/api/knowledge/search/utils.ts Outdated
Rank n in one leg always ties rank n in the other, so ordering the fused list
by score alone let whichever leg was scored first take every tied slot. At
topK=1 that meant a hybrid search returned exactly the vector-only result and
discarded the exact keyword match the mode exists to recover.

Selection now orders by score and drains each tie group round-robin, taking
from whichever leg has contributed fewest rows so far. The lexical leg is
passed first so it wins a total tie, since a chunk the vector leg ranked below
its distance threshold is the case hybrid was opted into for.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 930a1d4. Configure here.

Comment thread apps/sim/app/api/knowledge/search/utils.ts Outdated
Attributing a row found by both legs to a single leg left the round-robin
owing the other leg a slot it had already been served. With a shared rank-1
hit and topK 2, that evicted the lexical-only row — the exact match hybrid was
enabled to recover — in favor of the vector-only one.

A shared row satisfied every leg that returned it, so every one of them is now
charged for it. Tie-breaking prefers the candidate whose least-served leg has
been served least, which also removes the arbitrary best-rank attribution.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 318b236. Configure here.

The shared dispatch treats a whitespace-only query as absent and throws when no
tag filters accompany it, where the previous vector-only call would have
embedded the blank string and searched. Tighten the existing guard so the tool
returns its normal message instead.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/knowledge/search/utils.ts
The vector leg caps candidates per base once getQueryStrategy sets useParallel,
but the keyword leg always ran one global query with a single LIMIT. Searching
several bases at once let whichever one ranks strongest lexically consume every
slot, so an exact-token hit in a smaller base never reached fusion — the case
hybrid exists to serve.

The keyword leg now uses the same strategy: per-base queries under the same
parallel limit, re-ranked globally on a selected ts_rank_cd. Both legs draw
candidates the same way, so fusion combines rankings over the same pool.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/knowledge/search/route.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 03eda34. Configure here.

Selecting the cosine distance in the ranking query made Postgres detoast the
1536-dimension embedding and compute a distance for every full-text match
before the LIMIT applied, so cost tracked how common the query term was rather
than topK. On a 20k-chunk base with a term matching every row that was 61,055
buffer hits against 1,030 for the same query without the projection.

Rank on ids and ts_rank_cd alone, then hydrate only the rows that survive the
limit. Same results, and the worst case drops to ~27ms end to end.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f1a8e9d. Configure here.

@waleedlatif1
waleedlatif1 merged commit ed23330 into staging Jul 31, 2026
27 checks passed
@waleedlatif1
waleedlatif1 deleted the kb-hybrid-retrieval branch July 31, 2026 19:42
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