NIP-50

Search Capability

draft relay features

NIP-50 enables search on Nostr by allowing relays to support a 'search' parameter in filters for full-text content search, returning matching events.

Author
fiatjaf, mikedilger, monlovesmango
Last Updated
15 January 2024
Official Spec
View on GitHub →

NIP-50: Search Capability

Status: Draft Authors: fiatjaf, mikedilger, monlovesmango Category: Relay Features


Overview

NIP-50 defines how Nostr relays can provide search functionality, enabling users to search for events by content, keywords, or other criteria.

Core Concept:

  • Relays that support search add a search parameter to their filter capabilities
  • Clients include search in filter objects when querying
  • Relays return events matching the search query

Search Types:

  • Full-text search: Search event content
  • Keyword search: Match specific terms
  • Metadata search: Find profiles, tags
  • Combined search: Search + other filters

Status: Draft (not all relays implement this yet)


Why Search Matters

Discovery

Without search:

  • Users can only browse chronological timelines
  • Finding specific topics is difficult
  • Content discovery is limited to follows

With search:

  • Find past conversations
  • Discover trending topics
  • Locate specific users or content
  • Research topics across the network

Use Cases

  1. Content Discovery: “Find posts about Bitcoin”
  2. User Search: “Find users named Alice”
  3. Topic Research: “What are people saying about Nostr?”
  4. Historical Lookup: “Find that post I saw last week”
  5. Hashtag Tracking: “Show me all #nostr posts”
  6. Event Tracking: “Find mentions of this conference”

How It Works

Search Filter Parameter

Relays supporting NIP-50 accept a search parameter:

{
  "kinds": [1],
  "search": "bitcoin",
  "limit": 20
}

Behavior: Relay returns events where content matches “bitcoin”.


Basic Search Query

import { relayPool } from 'nostr-tools';

async function searchContent(query, relays, limit = 20) {
  const filters = {
    kinds: [1],      // Text notes
    search: query,   // Search parameter
    limit: limit
  };

  const events = await Promise.all(
    relays.map(relay => relay.list([filters]))
  );

  return events.flat();
}

// Usage
const results = await searchContent("bitcoin", myRelays, 50);
console.log(`Found ${results.length} posts about bitcoin`);

Search Capabilities

Search anywhere in event content:

const filters = {
  kinds: [1],
  search: "nostr protocol"  // Matches events containing these words
};

Relay Behavior:

  • Some relays: Exact phrase match
  • Some relays: Match all words (AND logic)
  • Some relays: Match any word (OR logic)

Best Practice: Check relay documentation for search behavior.


Search for specific hashtags:

const filters = {
  kinds: [1],
  search: "#bitcoin"  // Find all #bitcoin posts
};

Alternative: Use #t tag filter (NIP-01):

const filters = {
  kinds: [1],
  "#t": ["bitcoin"]  // More reliable than search
};

Search in kind 0 (metadata) events:

const filters = {
  kinds: [0],
  search: "Alice"  // Find users with "Alice" in name/about
};

Returns: Profile events matching the query.


4. Combined Filters

Combine search with other filter parameters:

const filters = {
  kinds: [1],
  authors: ["pubkey1", "pubkey2"],  // From specific authors
  search: "bitcoin",                 // Containing "bitcoin"
  since: 1673347337,                 // After this timestamp
  limit: 50
};

Behavior: Relay returns events matching ALL criteria.


Relay Implementation

Relays indicate NIP-50 support in their information document (NIP-11):

{
  "name": "relay.example.com",
  "supported_nips": [1, 11, 50],
  "search": {
    "available": true,
    "supported_kinds": [0, 1, 30023]
  }
}

Search Algorithms

Relays may implement search differently:

Simple Substring Matching:

SELECT * FROM events WHERE content LIKE '%bitcoin%'

Full-Text Search (PostgreSQL):

SELECT * FROM events WHERE to_tsvector(content) @@ to_tsquery('bitcoin')

Elasticsearch Integration:

const results = await elasticsearch.search({
  index: 'nostr-events',
  body: {
    query: {
      match: { content: query }
    }
  }
});

Client Implementation

Detecting Search Support

Check if relay supports search:

async function supportsSearch(relayUrl) {
  try {
    const info = await fetch(`https://${relayUrl.replace('wss://', '')}`, {
      headers: { 'Accept': 'application/nostr+json' }
    });

    const data = await info.json();

    return data.supported_nips?.includes(50) || false;
  } catch {
    return false;
  }
}

// Usage
const canSearch = await supportsSearch('wss://relay.damus.io');

Fallback Strategy

If relay doesn’t support search:

async function searchWithFallback(query, relays) {
  const searchRelays = [];
  const regularRelays = [];

  // Separate search-capable relays
  for (const relay of relays) {
    if (await supportsSearch(relay.url)) {
      searchRelays.push(relay);
    } else {
      regularRelays.push(relay);
    }
  }

  if (searchRelays.length > 0) {
    // Use search parameter
    return searchContent(query, searchRelays);
  } else {
    // Fallback: fetch recent events and filter client-side
    return clientSideSearch(query, regularRelays);
  }
}

function clientSideSearch(query, relays) {
  // Fetch recent events
  const events = await fetchRecent(relays, 1000);

  // Filter locally
  return events.filter(event =>
    event.content.toLowerCase().includes(query.toLowerCase())
  );
}

Advanced Search Patterns

Some relays may support advanced queries:

// AND search
const filters = { search: "bitcoin AND lightning" };

// OR search
const filters = { search: "bitcoin OR ethereum" };

// NOT search
const filters = { search: "bitcoin NOT scam" };

// Phrase search
const filters = { search: '"nostr protocol"' };

Note: Support varies by relay. Check documentation.


// Prefix search
const filters = { search: "bitco*" };  // Matches bitcoin, bitcoins, etc.

// Suffix search
const filters = { search: "*coin" };   // Matches bitcoin, dogecoin, etc.

Future extension (not standardized):

// Search specific fields
const filters = {
  search: "author:alice content:nostr"
};

Search UI Patterns

Search Bar Component

function SearchBar({ onSearch }) {
  const [query, setQuery] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    if (query.trim()) {
      onSearch(query);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="search-bar">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search Nostr..."
      />
      <button type="submit">
        🔍 Search
      </button>
    </form>
  );
}

Search Results Display

function SearchResults({ query, results, loading }) {
  if (loading) {
    return <div>Searching for "{query}"...</div>;
  }

  if (results.length === 0) {
    return <div>No results found for "{query}"</div>;
  }

  return (
    <div className="search-results">
      <h2>{results.length} results for "{query}"</h2>

      {results.map(event => (
        <SearchResult key={event.id} event={event} query={query} />
      ))}
    </div>
  );
}

Highlight Search Terms

function SearchResult({ event, query }) {
  function highlightText(text, query) {
    if (!query) return text;

    const regex = new RegExp(`(${query})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
  }

  return (
    <div className="search-result">
      <div
        className="content"
        dangerouslySetInnerHTML={{
          __html: highlightText(event.content, query)
        }}
      />
    </div>
  );
}

Search Relay Services

Specialized Search Relays

Some relays specialize in search:

nostr.band (wss://relay.nostr.band):

  • Advanced full-text search
  • Trending analysis
  • User/content discovery
  • Public API

Primal Cache (wss://cache1.primal.net):

  • Ultra-fast search
  • Curated trending
  • Optimized for Primal client

search.nos.today (experimental):

  • Elasticsearch-powered
  • Advanced query syntax
  • Aggregations and analytics

Using Multiple Search Relays

const searchRelays = [
  'wss://relay.nostr.band',
  'wss://cache1.primal.net',
  'wss://relay.damus.io'
];

async function multiRelaySearch(query) {
  const filters = {
    kinds: [1],
    search: query,
    limit: 100
  };

  const results = await Promise.all(
    searchRelays.map(relay => searchRelay(relay, filters))
  );

  // Flatten and deduplicate
  const allResults = results.flat();
  const unique = deduplicateByEventId(allResults);

  return unique;
}

Performance Considerations

Indexing

Relays should index searchable fields:

Database Indexes (PostgreSQL example):

-- GIN index for full-text search
CREATE INDEX idx_events_content_fts
ON events USING GIN (to_tsvector('english', content));

-- Trigram index for fuzzy search
CREATE INDEX idx_events_content_trgm
ON events USING GIN (content gin_trgm_ops);

Caching

Cache search results for popular queries:

const searchCache = new Map();

async function cachedSearch(query, relays) {
  const cacheKey = `${query}:${Date.now() / (60 * 1000)}`;  // 1min TTL

  if (searchCache.has(cacheKey)) {
    return searchCache.get(cacheKey);
  }

  const results = await searchContent(query, relays);
  searchCache.set(cacheKey, results);

  return results;
}

Pagination

For large result sets:

async function paginatedSearch(query, relays, page = 1, pageSize = 20) {
  const offset = (page - 1) * pageSize;

  const filters = {
    kinds: [1],
    search: query,
    limit: pageSize,
    offset: offset  // Not standard, but some relays support
  };

  return await searchContent(filters, relays);
}

Security & Abuse Prevention

Search Query Validation

Sanitize user input:

function sanitizeQuery(query) {
  // Remove potentially malicious characters
  return query
    .replace(/[<>]/g, '')      // Remove HTML tags
    .replace(/;/g, '')         // Remove SQL injection risks
    .slice(0, 200);            // Limit length
}

Rate Limiting

Prevent search abuse:

const searchRateLimit = new Map();

function canSearch(pubkey) {
  const now = Date.now();
  const lastSearch = searchRateLimit.get(pubkey) || 0;

  if (now - lastSearch < 5000) {  // 5 second cooldown
    return false;
  }

  searchRateLimit.set(pubkey, now);
  return true;
}

Spam Filtering

Filter low-quality search results:

function filterSpam(results, userFollows) {
  return results.filter(event => {
    // Prioritize events from followed users
    if (userFollows.has(event.pubkey)) {
      return true;
    }

    // Filter very short or repetitive content
    if (event.content.length < 10) {
      return false;
    }

    // Add more spam detection logic...
    return true;
  });
}

Limitations

1. Inconsistent Implementation

Different relays implement search differently:

  • Some: Full-text search
  • Some: Substring matching only
  • Some: No search support

Impact: Results vary by relay.


2. No Standard Query Syntax

No standardized way to specify:

  • Boolean operators (AND, OR, NOT)
  • Wildcards
  • Field-specific searches
  • Fuzzy matching

Impact: Advanced queries may not work across relays.


3. Scalability Challenges

Full-text search is resource-intensive:

  • Large indexes required
  • Slow on huge datasets
  • Expensive for relay operators

Impact: Not all relays can afford search support.


Client Support

  • Primal - Advanced search with trending
  • Amethyst - Built-in search functionality
  • Nostrudel - Power user search tools
  • nostr.band - Specialized search interface
  • Damus - No built-in search yet
  • Snort - Basic search only
  • Iris - Client-side filtering

Check our Client Directory for details.


Common Questions

Not all clients implement NIP-50 yet, and not all relays support it. Search is complex and resource-intensive.

Can I search private messages?

No. NIP-50 searches public events. Encrypted DMs (NIP-04) cannot be searched (by design).

How do I search for hashtags?

Use search: "#bitcoin" or use the #t tag filter: "#t": ["bitcoin"].

Why are search results incomplete?

You’re only searching relays you’re connected to. Different relays have different events.

Can relays index all of Nostr?

No. Each relay has its own subset of events. For comprehensive search, query multiple specialized search relays.


  • NIP-01 - Basic protocol (event structure, filters)
  • NIP-11 - Relay information document (advertising NIP-50 support)

Technical Specification

For the complete technical specification, see NIP-50 on GitHub.


Summary

NIP-50 enables search on Nostr:

Search parameter in filters ✅ Relay discretion on implementation ✅ Full-text search across content ✅ Combine with filters for targeted queries

Filter example:

{
  "kinds": [1],
  "search": "bitcoin",
  "limit": 20
}

Status: Draft - not universally supported yet.

Best practice: Check relay support, use specialized search relays, implement fallbacks.


Next Steps:


Last updated: January 2024 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

primal nostr-band amethyst nostrudel
View all clients →

Related NIPs

NIP-01 NIP-11
← Browse All NIPs