How Buzz Integrates Postgres FTS Full-Text Search: A Complete Technical Guide
Buzz integrates PostgreSQL full-text search through a dedicated buzz-search crate that uses generated tsvector columns, GIN indexes, and database triggers to enable fast, relevance-ranked search over Nostr events via both HTTP and WebSocket interfaces.
The Buzz Nostr relay implementation leverages native Postgres FTS capabilities to provide scalable full-text search across event content. This integration eliminates the need for external search engines by utilizing PostgreSQL's built-in tsvector and tsquery functionality, wrapped in a Rust API that maintains compatibility with NIP-50 search filters.
Database Schema and Indexing Strategy
The search_vector Column
At the foundation of Buzz's search architecture lies a generated column in the events table defined in crates/buzz-db/src/schema.sql. This column aggregates searchable fields—such as content, tags, and kind—into a PostgreSQL tsvector data type optimized for full-text queries.
To ensure query performance, the schema applies a GIN (Generalized Inverted Index) to the search_vector column. This index structure enables fast lookups when matching tsquery expressions against the indexed vectors, crucial for handling high-throughput Nostr event streams.
Automated Index Maintenance with Triggers
Buzz maintains search index consistency through database automation rather than application logic. The crates/buzz-db/src/triggers.sql file defines the events_search_vector_trigger, which executes on every insert or update operation.
This trigger invokes the to_tsvector function to recompute the search vector dynamically. By default, the system uses the simple dictionary, though this is configurable per deployment. This approach guarantees that the search_vector remains synchronized with the actual event data without requiring manual intervention or application-side batch updates.
Search Implementation in Rust
The search_events API
The core search logic resides in crates/buzz-search/src/lib.rs, which exposes the search_events function. This high-level Rust API constructs NIP-50-compatible filters and forwards them to the database layer through parameterized SQL queries.
When invoked, the function parses the user-supplied query string and converts it into a tsquery object. It then executes a SQL statement utilizing the @@ operator to match events where search_vector @@ tsquery, ensuring precise text matching against the indexed content.
Query Construction and Ranking
Results are ordered by relevance using PostgreSQL's ts_rank_cd function, which considers lexical proximity and frequency. The ranking algorithm accounts for the configured search language and applies the weights specified in environment variables, ensuring that the most pertinent events appear first in the result set.
Exposing Search to Clients
HTTP REST Endpoint
The relay exposes search functionality through the /search endpoint implemented in crates/buzz-relay/src/http.rs. This endpoint accepts POST requests with a JSON payload containing query (string), limit (optional u32), and offset (optional u32).
Upon receiving a request, the HTTP handler validates the input parameters and delegates to the search_events function. Results are serialized into the standard Nostr event format and returned to the client, maintaining compatibility with existing Nostr tooling.
WebSocket NIP-50 Support
Clients can issue search requests through WebSocket connections using the REQ message type with a NIP-50 search filter. The WebSocket handler in crates/buzz-relay/src/ws.rs translates these filters into equivalent calls to the search_events function, ensuring uniform behavior across HTTP and WebSocket transport layers.
Configuration and Language Support
Buzz provides runtime configuration for search behavior through environment variables. The BUZZ_SEARCH_LANGUAGE variable controls which PostgreSQL text search dictionary applies (options include english, simple, or other installed languages), while BUZZ_SEARCH_RANKING adjusts the ranking algorithm parameters.
Default configurations and advanced tuning options are documented in crates/buzz-search/README.md, allowing operators to optimize search relevance for specific content domains without modifying source code.
Usage Examples
Rust Client SDK
use buzz_sdk::client::BuzzClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = BuzzClient::new("ws://localhost:3000", "your_private_key")?;
let results = client.search("rust async programming").await?;
for ev in results {
println!("{} – {}", ev.id, ev.content);
}
Ok(())
}
CLI Search
# Search via the CLI (default language is "english")
buzz search --query "project management" --limit 10
Direct HTTP Request
curl -X POST http://localhost:3000/search \
-H "Content-Type: application/json" \
-d '{"query":"blockchain","limit":5}'
Summary
- Database Layer: Buzz uses a generated
tsvectorcolumn (search_vector) with a GIN index and PostgreSQL triggers for automatic index maintenance. - Core Library: The
buzz-searchcrate incrates/buzz-search/src/lib.rsprovides thesearch_eventsfunction that buildstsqueryobjects and ranks results usingts_rank_cd. - API Exposure: Search is available via HTTP
POST /searchincrates/buzz-relay/src/http.rsand WebSocket NIP-50 filters incrates/buzz-relay/src/ws.rs. - Configuration: Language dictionaries and ranking weights are configurable through
BUZZ_SEARCH_LANGUAGEandBUZZ_SEARCH_RANKINGenvironment variables. - NIP-50 Compliance: Both transport methods support standard Nostr search filters, ensuring interoperability with existing clients.
Frequently Asked Questions
How does Buzz maintain the full-text search index when events are updated?
Buzz utilizes PostgreSQL triggers defined in crates/buzz-db/src/triggers.sql to automatically recompute the tsvector column whenever an event is inserted or modified. The events_search_vector_trigger invokes to_tsvector on the relevant fields, ensuring the search index remains synchronized without application-level intervention.
What search syntax does Buzz support for queries?
Buzz leverages PostgreSQL's native tsquery parsing capabilities, supporting lexeme-based searching with logical operators (&, |, !) and phrase grouping. The default simple configuration performs basic tokenization, while language-specific dictionaries enable stemming and stop-word filtering when configured via BUZZ_SEARCH_LANGUAGE.
Can operators customize the search ranking algorithm?
Yes. Operators can adjust search result ordering through the BUZZ_SEARCH_RANKING environment variable, which modifies parameters passed to PostgreSQL's ts_rank_cd function. The crates/buzz-search/README.md file documents available tuning options for relevance weighting and normalization strategies.
Is the search functionality compatible with standard Nostr clients?
Yes. Buzz implements NIP-50 search filters, allowing standard Nostr clients to submit search queries through WebSocket REQ messages. Additionally, the HTTP /search endpoint provides an alternative transport for clients that prefer REST APIs, both returning events in the canonical Nostr format.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →