Skip to content

Search perf, NIP fixes, replying-to usernames, highlights & mentions filter#135

Merged
dergigi merged 18 commits intodergigi:masterfrom
alltheseas:perf/fixes-with-benchmarks
Mar 15, 2026
Merged

Search perf, NIP fixes, replying-to usernames, highlights & mentions filter#135
dergigi merged 18 commits intodergigi:masterfrom
alltheseas:perf/fixes-with-benchmarks

Conversation

@alltheseas
Copy link
Copy Markdown
Contributor

@alltheseas alltheseas commented Feb 18, 2026

Summary

  • Fix multiple search and relay discovery bugs (NIP-51 tag parsing, extension-only queries, fallback relay injection)
  • Performance improvements to search pipeline (parallel OR terms, parallel author resolution, incremental pagination, scoped relay pings)
  • Show "replying to @username" in note headers, highlight search terms in results, add mentions: search filter
  • Improve profile and parent event resolution reliability

Bug fixes

  • NIP-51 tag parsing: kinds 10006/10007 now correctly parse relay tags (per spec) in addition to r tags
  • Extension-only queries: language:en no longer drops the search field when base query is empty
  • Fallback relay injection: fallback relays now verify NIP-50 support before being added
  • Profile resolution: shared profile resolver with profile relay fallback for more reliable author display
  • Parent event resolution: improved reliability of fetching parent events for reply threads

Performance

  • Parallel OR term search via Promise.allSettled
  • Parallel author resolution via Promise.all
  • Scoped relay ping measurement (per relay, not global)
  • O(n) Set-based deduplication replacing O(n^2) findIndex
  • Incremental pagination for search results

Features

  • Show "replying to @username" in NoteHeader
  • Highlight search terms in results
  • Add mentions: search filter for #p tag queries

Test plan

  • Search with OR terms returns results from all terms
  • language:en extension-only query returns results
  • Profile images load reliably for search results
  • "replying to" shows correct username in reply notes
  • Search term highlighting visible in results
  • mentions:npub... filter returns tagged events

🤖 Generated with Claude Code

alltheseas and others added 13 commits February 18, 2026 04:42
Replace sequential for...of loop in searchByAnyTerms with
Promise.allSettled so that each OR term is searched concurrently.

Pre-resolve fallback relay set once before parallel execution to
eliminate the memoization race in ensureFallbackRelaySet. Individual
term failures are isolated and don't kill the batch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 3 serial for...of author resolution loops with Promise.all
so that multiple by: tokens are resolved concurrently. Each token
is individually wrapped in try/catch — decode failures return null
and are filtered out, matching the previous skip-and-warn behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract measureRelayPing and measureAllRelayPings into a new
src/lib/relayPing.ts module with injectable dependencies for
testability.

The key fix: each ping subscription now receives a relaySet scoped
to the single target relay (NDKRelaySet.fromRelayUrls([relayUrl])),
so the REQ is sent only to the relay being measured. Previously the
subscription went to all connected relays, making ping measurements
inaccurate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show first 50 results initially with a "Show more" button that loads
50 more at a time. Pagination resets only when the search query
changes, not when results mutate (e.g. NIP-05 verified reordering),
avoiding the infinite-reset loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The result deduplication in searchEvents used
arr.findIndex(x => x.id === e.id) which is O(n) per element,
yielding O(n^2) total. Replace with a Set<string> lookup for O(n).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NIP-51 specifies 'relay' tags for blocked relay lists (kind 10006)
and search relay lists (kind 10007). The code was only checking for
'r' tags, which is the NIP-65 convention for kind 10002.

Now checks for both 'relay' (per spec) and 'r' (for compatibility
with mis-tagged events in the wild). Without this fix, user-configured
blocked relays and search relays were silently ignored.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
filterNip50Relays was injecting fallback relays (primal, snort,
ditto) without verifying they actually support NIP-50. This caused
non-NIP-50 relays to receive search: filters and return extraneous
results.

Now runs checkNip50Support on each fallback candidate before adding
it to the supported list, matching the verification applied to the
primary relay set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Queries like 'language:en' were silently dropped because:
1. buildSearchQueryWithExtensions returned early on empty baseQuery
2. search.ts used 'cleanedQuery || undefined' which made '' falsy

Now buildSearchQueryWithExtensions produces extension-only strings
when extensions are present, and the caller always invokes it so
that extension-only queries reach the relay as valid search filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds mentions:<user> syntax to find events that mention a specific user
via NIP-27 p-tags. Supports multiple mentions: tokens and combines with
by: and free-text search terms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Highlights matching search terms in note content with a blue background.
Extracts terms from the query (excluding filter tokens like by:, kind:,
mentions:) and wraps matches in <mark> elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace raw nevent identifiers with resolved display names for reply
parent authors. Uses e-tag[4] pubkey when available (NIP-10), falls back
to fetching the parent event with relay hint from e-tag[2]. Includes
module-level profile cache with TTL, in-flight deduplication, and
IntersectionObserver for lazy resolution of off-screen notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract resolveProfileName to shared profileUtils so NoteHeader,
InlineNostrToken, and EventCard all use the same cached resolver.
When default relays return no profile, retry against profile-specific
relays (purplepag.es, search.nos.today, relay.nostr.band). Also fixes
double npub:npub1... prefix in fallback display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 6s timeout to parent event fetch to prevent hanging
- Try relay hint first, then fall back to default relays
- Use relaySets.profileSearch() instead of hardcoded RELAYS.PROFILE_SEARCH
  to respect user's blocked relay preferences (NIP-51 kind 10006)
- EventCard InlineAuthor now uses shared resolveProfileName for
  consistent caching and profile relay fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Feb 18, 2026

@alltheseas is attempting to deploy a commit to the dergigi's projects Team on Vercel.

A member of the Team first needs to authorize it.

@alltheseas alltheseas changed the base branch from main to master February 18, 2026 11:09
@alltheseas
Copy link
Copy Markdown
Contributor Author

Screenshot 2026-02-18 at 4 21 11 AM Screenshot 2026-02-18 at 5 12 17 AM Screenshot 2026-02-18 at 5 13 25 AM Screenshot 2026-02-18 at 5 14 03 AM

@alltheseas
Copy link
Copy Markdown
Contributor Author

Screenshot 2026-02-18 at 5 15 59 AM Screenshot 2026-02-18 at 5 16 17 AM

@dergigi
Copy link
Copy Markdown
Owner

dergigi commented Mar 13, 2026

Just saw this now 😅 I'll try to have a look soon
Thanks for the contributions!

@alltheseas
Copy link
Copy Markdown
Contributor Author

all good, I think ants are the best hope for non-native search at this point. no pressure.

will submit nip-66 upgrades based on my nostrability/outbox nip-66 learnings shortly as well.

during my outbox benchmarks I found that less than 1/2 relays return real info (many dead/offline/non-responsive). by adding nip-66 to outbox algos as a pre-filter for relay liveness, this reduces load times in the range of -40% to -50%. i suspect there will be benefit with search as well.

alltheseas and others added 5 commits March 13, 2026 09:59
Configuration for NIP-66 relay liveness integration:
- 30-min cache/refresh interval (monitors publish hourly)
- 15s fetch timeout
- 80% safety threshold to prevent catastrophic filtering
- 24h max age for dead relay entries before degrading to unknown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New module that fetches kind 30166 events from relay monitors to
determine relay liveness, RTT, and NIP support. Key design decisions:

- Asymmetric staleness: alive entries trusted indefinitely (false
  positive = just a timeout), dead entries expire after 24h and
  degrade to 'unknown' (false negative = missed results)
- Safety valve: if filtering would remove >80% of relays, skip
  entirely to prevent catastrophic failure from bad monitor data
- Multiple sources: queries known monitor relays + user's connected
  NDK pool relays for broader coverage
- Persistent cache via localStorage with 30-min background refresh
- Concurrent call coalescing to prevent duplicate fetches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire NIP-66 data into the relay selection pipeline:

- filterNip50Relays: pre-filter dead relays before probing, use
  NIP-66 N tags as fast path to skip HTTP NIP-11 probes
- getNip50SearchRelaySet: enrich candidates with NIP-66-discovered
  NIP-50 relays, remove dead debug loop
- getBroadRelaySet: filter dead relays from broad set
- getOutboxSearchCapableRelays: pre-filter author's write relays
  before expensive NIP-11 probing
- connect(): fire-and-forget ensureNip66Data() so cache is populated
  before first search
- Relay monitoring lifecycle: start/stop NIP-66 background refresh

All integration points use only cached data (synchronous lookups).
NIP-66 never blocks search — if cache is empty, all functions fall
through to existing behavior unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display monitor data next to each relay in the UI:
- Green [45ms] when monitor reports alive with RTT
- Red [dead] when monitor reports dead
- Nothing when no monitor data available

Visual indicator only — no behavioral change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Self-contained benchmark script measuring NIP-66 relay liveness
filtering impact across four phases:

- Phase A: HTTP-only NIP-11 probing (3-endpoint, no NIP-66)
- Phase B: NIP-66 pre-filtering + NIP-50 fast path
- Phase C: actual NIP-50 search comparison (opt-in via --search)
- Phase D: WebSocket connection timing with/without filtering

Key findings from benchmark runs:
- 51 NIP-50 relays discovered via NIP-66 beyond 7 hardcoded
- 48% fewer HTTP probes (dead relay filtering + fast path)
- +8% search event recall from extra NIP-50 relay discovery
- 1 NIP-50 relay rescued per run (HTTP timeout, NIP-66 knows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alltheseas alltheseas changed the title Search perf, NIP fixes, replying-to usernames, highlights & mentions filter NIP-66 relay liveness filtering + perf fixes + benchmark Mar 13, 2026
@alltheseas alltheseas changed the title NIP-66 relay liveness filtering + perf fixes + benchmark Search perf, NIP fixes, replying-to usernames, highlights & mentions filter Mar 13, 2026
@alltheseas
Copy link
Copy Markdown
Contributor Author

here is the follow-on nip-66 PR: #136

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 15, 2026

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

Project Deployment Actions Updated (UTC)
ants Ready Ready Preview, Comment Mar 15, 2026 11:18am

Request Review

Copy link
Copy Markdown
Owner

@dergigi dergigi left a comment

Choose a reason for hiding this comment

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

Solid PR — NIP-51 tag parsing fix, parallel search, scoped relay pings, profile resolution cleanup, search highlighting, pagination, and mentions filter all look good.

Two nits:

  1. Dead ternary in EventCard.tsxresult.isNpubFallback ? result.display : result.display — both branches are identical, can simplify to just result.display.

  2. bench/nip66.ts seems to have leaked from the NIP-66 branch (#136) into this PR.

Otherwise, nice work. The NIP-51 relay tag fix alone is worth merging. Perf improvements are real — Set-based dedup and parallel OR search are solid wins.

Recommend: merge this first, then #136 stacks cleanly on top.

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