Search Capability
NIP-50 enables search on Nostr by allowing relays to support a 'search' parameter in filters for full-text content search, returning matching events.
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
searchparameter to their filter capabilities - Clients include
searchin 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
- Content Discovery: “Find posts about Bitcoin”
- User Search: “Find users named Alice”
- Topic Research: “What are people saying about Nostr?”
- Historical Lookup: “Find that post I saw last week”
- Hashtag Tracking: “Show me all #nostr posts”
- 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
1. Full-Text Search
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.
2. Hashtag Search
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
};
3. User 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
Supporting Search
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
Boolean Search
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.
Wildcard Search
// Prefix search
const filters = { search: "bitco*" }; // Matches bitcoin, bitcoins, etc.
// Suffix search
const filters = { search: "*coin" }; // Matches bitcoin, dogecoin, etc.
Field-Specific Search
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
Clients with Search
- Primal - Advanced search with trending
- Amethyst - Built-in search functionality
- Nostrudel - Power user search tools
- nostr.band - Specialized search interface
Limited/No Search
- Damus - No built-in search yet
- Snort - Basic search only
- Iris - Client-side filtering
Check our Client Directory for details.
Common Questions
Why doesn’t my favorite client have search?
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.
Related NIPs
- 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:
- Learn about Lightning zaps in NIP-57
- Explore relay lists in NIP-65
- Understand relay info in NIP-11
- Browse all NIPs in our reference
Last updated: January 2024 Official specification: GitHub
Client Support
This NIP is supported by the following clients: