# How Buzz Integrates Postgres FTS Full-Text Search: A Complete Technical Guide

> Learn how Buzz integrates Postgres FTS full-text search using generated tsvector columns, GIN indexes, and triggers for fast, relevance-ranked search over Nostr events.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: deep-dive
- Published: 2026-08-29

---

**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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/crates/buzz-search/README.md), allowing operators to optimize search relevance for specific content domains without modifying source code.

## Usage Examples

### Rust Client SDK

```rust
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

```bash

# Search via the CLI (default language is "english")

buzz search --query "project management" --limit 10

```

### Direct HTTP Request

```bash
curl -X POST http://localhost:3000/search \
     -H "Content-Type: application/json" \
     -d '{"query":"blockchain","limit":5}'

```

## Summary

- **Database Layer**: Buzz uses a generated `tsvector` column (`search_vector`) with a GIN index and PostgreSQL triggers for automatic index maintenance.
- **Core Library**: The `buzz-search` crate in [`crates/buzz-search/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-search/src/lib.rs) provides the `search_events` function that builds `tsquery` objects and ranks results using `ts_rank_cd`.
- **API Exposure**: Search is available via HTTP `POST /search` in [`crates/buzz-relay/src/http.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/http.rs) and WebSocket NIP-50 filters in [`crates/buzz-relay/src/ws.rs`](https://github.com/block/buzz/blob/main/crates/buzz-relay/src/ws.rs).
- **Configuration**: Language dictionaries and ranking weights are configurable through `BUZZ_SEARCH_LANGUAGE` and `BUZZ_SEARCH_RANKING` environment 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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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.