Skip to content

feat(search): hybrid vector⊕full-text retrieval (RRF) with graceful fallback - #93

Merged
bryanfawcett merged 4 commits into
mainfrom
claude/search-hybrid-upgrade
Jul 28, 2026
Merged

feat(search): hybrid vector⊕full-text retrieval (RRF) with graceful fallback#93
bryanfawcett merged 4 commits into
mainfrom
claude/search-hybrid-upgrade

Conversation

@bryanfawcett

Copy link
Copy Markdown
Contributor

Summary

  • Upgrades event search from "vector-only with a regex fallback" to a hybrid ranker — semantic $vectorSearch fused with typo-tolerant Atlas $search full-text — that degrades gracefully at every tier, so it improves transparently as the Mukoko platform creates the search indexes.
  • Ships the app-side wins now (unblocked); documents the DB-side asks that are platform-owned.

Type of Change

  • New feature (non-breaking change that adds functionality)

Context

Today search only runs vector search with a regex fallback — and the event_vector_index isn't present on the cluster, so in practice every query falls back to a brittle exact-substring regex scan. The headline MongoDB features (native $rankFusion hybrid, automated embeddings) need a MongoDB 8.1 upgrade (cluster is 8.0.x) and platform-created indexes — neither can ship from this repo. So this PR delivers the equivalent quality from app code and files the platform requests.

Changes

  • src/lib/mongo/search.ts — rewritten around ranked-id retrievers + Reciprocal Rank Fusion:
    1. HYBRID$vectorSearch ⊕ Atlas $search (fuzzy), fused via reciprocalRankFuse (RRF-60, exported + unit-tested), both run concurrently.
    2. VECTOR — semantic only (text index absent).
    3. TEXT — Atlas $search only (gateway/vector absent).
    4. REGEX — the existing scan, needs no index, last resort.
      Every tier is non-throwing for missing gateway/index. Adds autocompleteEventNames ($search autocomplete → regex-prefix fallback).
  • src/app/actions/search.tsSearchMode ("hybrid" | "vector" | "text"); new autocompleteEventsAction for type-ahead.
  • src/app/search/page.tsx — push a single active category into the server query (index-side narrowing) rather than only client-side filtering.
  • docs/search-platform-requests.md — the two indexes the platform must create (vector + full-text), plus the later 8.1 / native $rankFusion and automated-embedding (Voyage) notes.

Why app-side RRF: native $rankFusion needs MongoDB 8.1; swap to it once the cluster is upgraded (localized change, same result contract).

Test Plan

  • Existing tests pass (npm run test:run — 714 passed / 97 files)
  • New tests added (search.test.ts — RRF scoring/ordering/dedupe/weights)
  • Lint passes (npm run lint — 0 errors)
  • Build succeeds (npm run build)

Platform follow-ups (see docs/search-platform-requests.md)

  1. Create event_vector_index on events.eventEmbeddings (768-dim cosine + city/category filters) — unblocks semantic search + backfill embeddings.
  2. Create events_text_index (Atlas Search) on events.events — unblocks fuzzy full-text + autocomplete.
  3. Later: upgrade cluster to 8.1 for native $rankFusion; evaluate automated (Voyage) embeddings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RH8QR1QYMnMHDDW62K2XSj


Generated by Claude Code

Search only ever ran semantic vector search with a regex fallback — and the
vector index isn't present on the cluster, so in practice every query fell back
to a brittle exact-substring regex scan. Upgrade the retrieval layer to a
hybrid ranker that degrades cleanly at every tier:

  1. HYBRID — $vectorSearch fused with Atlas $search (full-text, typo-tolerant)
     via Reciprocal Rank Fusion in app code (both retrievers run concurrently).
  2. VECTOR — semantic only, when the text index is absent/empty.
  3. TEXT   — Atlas $search only, when the gateway/vector index is absent.
  4. REGEX  — the existing case-insensitive scan, needs no index, last resort.

Each tier is non-throwing for the expected failure modes (no gateway, missing
index) so search never hard-fails — and it transparently improves as the
platform creates the indexes.

- `src/lib/mongo/search.ts`: rewrite around ranked-id retrievers + RRF
  (`reciprocalRankFuse`, exported + unit-tested); add `atlasTextSearchIds`
  ($search + fuzzy), split vector into `vectorSearchIds`, keep the regex path;
  add `autocompleteEventNames` ($search autocomplete → regex-prefix fallback).
  `mode` gains `"hybrid"`.
- `src/app/actions/search.ts`: `SearchMode` type; new `autocompleteEventsAction`
  for type-ahead.
- `src/app/search/page.tsx`: push a single active category into the server
  query (index-side narrowing) instead of only client-side filtering.
- `docs/search-platform-requests.md`: the two indexes the Mukoko platform must
  create to light this up (vector + full-text), plus the later 8.1 upgrade
  (native $rankFusion) and automated-embeddings notes.

App-side RRF is used deliberately because native $rankFusion needs MongoDB 8.1
and the cluster is 8.0.x; swap once upgraded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH8QR1QYMnMHDDW62K2XSj
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
nhimbe Ready Ready Preview, Comment Jul 28, 2026 7:32am
nhimbe-admin Ready Ready Preview, Comment Jul 28, 2026 7:32am

Request Review

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

Reworded so no wrapped prose line starts with "+ `$search`", which
markdownlint read as a plus-style list marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH8QR1QYMnMHDDW62K2XSj
@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

The regex-metacharacter escape was duplicated in regexSearch and
autocompleteEventNames; extract a single escapeRegex() helper. No behavior
change — verified by search.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH8QR1QYMnMHDDW62K2XSj
@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

bryanfawcett pushed a commit that referenced this pull request Jul 26, 2026
A wrapped sentence put "#93" at the start of a line, which markdownlint
parsed as a missing-space ATX heading. Reword so no line begins with #.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C56AXtdhSMbh1pbouNZHAL
bryanfawcett added a commit that referenced this pull request Jul 27, 2026
…t-5 follow-ups (#101)

* chore(#100): remove extracted MCP worker + admin app, rewire, add follow-ups

The Mukoko Events MCP server and Mukoko Events Admin app now live in their
own repos (nyuchi/mukoko-events-mcp, nyuchi/mukoko-events-admin). This
removes them from the nhimbe repo and rewires everything so the public app
still builds/lints/tests green.

Removals:
- Delete worker/ and admin/ directories.
- Delete the distribution surfaces that travel with the MCP repo:
  plugins/, connectors/, top-level skills/, and .claude-plugin/marketplace.json.

Rewiring:
- package.json: drop the "admin" npm workspace; regenerate package-lock.json
  with no admin/worker entries.
- .github/workflows/ci.yml: remove the admin (Lint & Build, Tests) and worker
  (Tests, Type Check) jobs; the app's Lint & Build + Frontend Tests remain.
- eslint.config.mjs / tsconfig.json / .markdownlint-cli2.jsonc: drop the
  worker/.wrangler and admin/** ignores/excludes.
- next.config.ts: unchanged — the /admin* -> ADMIN_URL redirect stays by design.
- .claude/ dev tooling (launch.json, deploy skill, security-reviewer) updated
  to point at the new repos.

Docs:
- CLAUDE.md, README.md, AGENTS.md, RELEASES.md, docs/README.md rewritten where
  they described worker/, admin/, and the distribution surfaces — now pointing
  at nyuchi/mukoko-events-mcp and nyuchi/mukoko-events-admin. The shared Mongo
  admin-read layer (src/lib/mongo/admin.ts, admin-types.ts) stays here for the
  admin app to consume.

Part-5 investigations (docs/issue-100-followups.md):
- Palette autocomplete swap is BLOCKED on #93 (autocompleteEventsAction does not
  exist on this branch); documented the exact two-line change for later.
- SSR-first sweep of /search, /my-events, /calendar, /profile — evaluation only,
  no conversions; /my-events is the best SSR candidate.
- MongoDB "repeated things" — captured what was swept clean and what a
  DB-connected follow-up still needs.

Verified: npm run lint (0 errors), npm run test:run (716 passed / 97 files),
npm run build (succeeds with placeholder env). Design-token guard green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C56AXtdhSMbh1pbouNZHAL

* fix(docs): satisfy markdownlint MD018 in issue-100 followups

A wrapped sentence put "#93" at the start of a line, which markdownlint
parsed as a missing-space ATX heading. Reword so no line begins with #.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C56AXtdhSMbh1pbouNZHAL

* docs: correct the UI registry to Mzizi (mzizi.dev), not registry.mukoko.com

The design-system registry is Mzizi (mzizi.dev, shadcn-compatible), resolved by
the mzizi add CLI / Mzizi MCP. Rename the "Mukoko Registry" section, fix the
registry.mukoko.com URL, and register @mzizi in components.json so the config
matches the doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C56AXtdhSMbh1pbouNZHAL

* docs: fix stale worker/admin and self-hosted-auth references post-#100 split

Reconcile the last present-tense references to the extracted worker/ and
admin/ directories (issue #100) and one removed auth dir:

- RELEASES.md: worker/ "is now nhimbe-mcp" -> former dir became the Mukoko
  Events MCP, later extracted; drop the standalone `admin/` dir path.
- docs/plans/README.md + hono-migration.md banner: worker/ extracted to
  nyuchi/mukoko-events-mcp (banners are archival meta; plan bodies kept).
- .claude/settings.json: drop the stale *worker/* eslint-hook exclusion.
- .claude/agents/security-reviewer.md: auth is the hosted AuthKit UI
  (/auth/hosted + /callback), not the removed self-hosted src/app/auth/signin/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C56AXtdhSMbh1pbouNZHAL

---------

Co-authored-by: Bryan Fawcett <noreply@anthropic.com>
@bryanfawcett
bryanfawcett marked this pull request as ready for review July 28, 2026 07:31

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@bryanfawcett
bryanfawcett merged commit 0caa89f into main Jul 28, 2026
15 of 16 checks passed
@bryanfawcett
bryanfawcett deleted the claude/search-hybrid-upgrade branch July 28, 2026 07:32
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.

2 participants