⌨ Keyboard shortcuts available
G — waiting for next key…
Laravel Vector Search 17 min read

The Push-Based RAG: Building a Semantic Alerting Engine with Kafka and pgvector

Learn how to build a push-based RAG engine using Kafka and pgvector. This reverse-search architecture matches data streams against user intent vectors for real-time notifications.

P

Pradeep Bhandari

 · 1 views

Branded cover card: The Push-Based RAG: Building a Semantic Alerting Engine with Kafka and pgvector

Stop thinking about RAG as a reactive chatbot interface. In a modern semantic alerting system architecture, users’ interests are the static corpus, and the incoming data stream is the "query." This reverse-search pattern turns a passive data lake into an active notification engine that triggers action whenever new data matches user intent.

By piping events—news feeds, logs, or financial ticks—through Kafka into a processing microservice, you can vectorize fleeting events using models like all-MiniLM-L6-v2 or text-embedding-3-small. These are then matched against a pgvector index of user-defined "intent vectors." This setup avoids the "update storm" common in high-frequency streams by managing state with HNSW indexes and cooldown logic, preventing alert fatigue while maintaining low latency.

Flipping the RAG Script

Standard RAG is a "pull" system: a user asks a question, the system fetches context. This is too reactive for real-time monitoring. For high-scale environments like financial intelligence, you need a "push" flow where data searches for interested users. This transition is the core of semantic alerting.

From Pull to Push

Instead of a single query scanning a database, treat every incoming document as a query. Imagine a neighborhood watch where residents have specific concerns; rather than residents checking the street, an automated system identifies a broken window and immediately alerts only those who care about property damage. You are moving from a passive library to an intelligent, active notification hub.

The Reverse Search Pattern

Using Kafka and pgvector allows you to keep user intent vectors adjacent to profile data, ensuring consistency. When a user updates preferences in your backend, the change is immediately live for the next incoming event. Configure your database to treat user preferences as the searchable corpus with a schema that links embeddings to notification settings:

CREATE TABLE user_intents (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    intent_description TEXT,
    embedding vector(1536), -- Match your model dimensions
    cooldown_until TIMESTAMP,
    filters JSONB -- For metadata like 'region' or 'priority'
);

In this architecture, you index user intents. When a document arrives, vectorize it and run a similarity check against user_intents. The goal is determining who the document matters to, rather than just what it means.

Step 1: Modeling User Intent Vectors

In this setup, user preferences are stationary targets. We store mathematical representations of desire as "intent vectors."

Schema Design for Preference Vectors

Capturing user preference requires higher resolution than simple search. While 512-dimension models work for basics, 1536-dimension models (like OpenAI’s text-embedding-3-small) better distinguish between overlapping interests, such as "sustainable fashion" versus "luxury brands." Your table should associate user_id with these latent interests and relevant metadata.

Native pgvector Support in Laravel

If you are using Postgres, you can manage these vectors directly within your application framework. After installing the pgvector extension, use a migration to add the vector column:

Schema::create('user_interests', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->vector('interest_vector', 1536); // The meat of the engine
    $table->text('source_text')->nullable(); // What generated this vector?
    $table->timestamps();
});

With the foundation set, you can execute raw queries using the <=> cosine distance operator. The objective is to calculate the distance between an incoming document's vector and the user_interests table efficiently.

Step 2: Piping the Stream with Kafka

To feed the system in real-time, use Kafka as your high-pressure data pipe. This avoids the delays inherent in polling databases for new rows and allows you to listen to the infrastructure's pulse.

Connecting the Laravel-Kafka Consumer

For high-scale processing, use a long-running daemon via php-rdkafka. This process stays alive to consume messages from the content-ingress topic. Generate a worker using php artisan make:command KafkaContentConsumer and implement the consumer loop in the handle() method:

$conf = new \RdKafka\Conf();
$conf->set('group.id', 'semantic-alerter-group');
$conf->set('metadata.broker.list', env('KAFKA_BROKERS'));
$conf->set('auto.offset.reset', 'earliest');

$consumer = new \RdKafka\KafkaConsumer($conf);
$consumer->subscribe(['content-ingress']);

while (true) {
    $message = $consumer->consume(120 * 1000);
    switch ($message->err) {
        case RD_KAFKA_RESP_ERR_NO_ERROR:
            $this->processPayload(json_decode($message->payload, true));
            break;
        case RD_KAFKA_RESP_ERR__TIMED_OUT:
            // No message? No problem.
            break;
        default:
            throw new \Exception($message->errstr(), $message->err);
    }
}

Handling High-Throughput Payloads

Incoming documents typically arrive as JSON. Use kcat to verify the connection: echo '{"raw_text": "New market trends in AI", "source": "news_feed"}' | kcat -P -b localhost:9092 -t content-ingress. To minimize ingestion lag, the consumer should be lean, acknowledging messages and preparing text for vectorization without performing heavy computations immediately.

Step 3: The JIT Vectorization Hook

Once a message is captured, it must be converted into a mathematical coordinate. This is the bottleneck of the alerting pipeline.

Synchronous vs. Asynchronous Embedding

While background jobs are standard, vectorization within the Kafka consumer loop is often preferable to avoid complex state management and race conditions. Synchronous calls ensure the Kafka offset only moves if the vector is successfully generated, preventing data loss during provider downtime.

$content = $kafkaMessage->payload['body'];

// Synchronous call to your embedding provider
$vector = AI::vectorize($content, connection: 'openai-3-small');

if (!$vector) {
    Log::error("Failed to vectorize message ID: " . $kafkaMessage->id);
    return;
}

Avoiding Ingestion Lag

Do not use database triggers for vectorization. They are opaque and tie up database connection pools while waiting for external API I/O. Performing "Pre-Vectorization" at the consumer level ensures the database only handles clean, ready-to-query floats, keeping pgvector operations fast.

Step 4: Executing the Reverse Search Query

With the document vector ready, the system asks: "Which users have an interest profile matching this content?"

The Match-All Logic

Using pgvector, use the cosine distance operator (<=>) with a threshold rather than a LIMIT. For text-embedding-3-small, a distance under 0.15 is typically the sweet spot for relevance.

$matches = DB::table('user_interests')
    ->select('user_id')
    ->whereRaw('embedding <=> ? < 0.15', [$documentVector])
    ->pluck('user_id');

Threshold-Based Filtering

To avoid expensive vector math across millions of users, apply "hard" filters first. If a document is categorized as "FinTech" in "English," only compare it against users interested in those specific metadata tags. Chaining SQL WHERE clauses reduces the rows the vector engine must process, dropping query times significantly.

$relevantUsers = DB::table('user_interests')
    ->where('category', $incomingDoc->category)
    ->where('language', $incomingDoc->lang)
    ->whereRaw('embedding <=> ? < 0.15', [$documentVector])
    ->pluck('user_id');

Step 5: Managing Alert Fatigue and State

High-frequency events can trigger redundant notifications. You need a circuit breaker—a de-duplication layer—between the vector match and the dispatcher.

The Redis De-duplication Layer

Use Redis to track recently sent alerts. Instead of exact ID matches, look for semantic clusters. Generate a hash of the content or the intent vector and check it against a Redis key with a Time-To-Live (TTL). If the key exists, drop the event to prevent notification spam.

Stateful Alerting Windows

Use the SET EX command with the NX flag to ensure atomicity. Create a composite key using the user_id and a topic hash. This ensures the user receives one high-signal alert for a cluster of similar documents rather than a flurry of redundant pings.

$throttleKey = "alert:{$userId}:" . md5($semanticTopic);

// 'NX' ensures we only set the key if it doesn't exist
if (Redis::set($throttleKey, true, 'EX', 600, 'NX')) {
    // Key was set, so this is a fresh match
    $this->dispatchNotification($user, $document);
} else {
    // Key exists, user was notified within the last 10 minutes
    Log::info("Throttling redundant alert for user {$userId}");
}

Step 6: Indexing for Speed (HNSW on Intents)

Brute-force similarity checks will eventually lag the Kafka consumer. For dynamic alerting, HNSW (Hierarchical Navigable Small Worlds) is superior to IVFFlat because it requires no training step and maintains accuracy as data shifts.

CREATE INDEX ON user_interests 
USING hnsw (embedding vector_cosine_ops) 
WITH (m = 16, ef_construction = 64);

Tuning m and ef_construction

Set m to 16 for a balance between search speed and memory usage. Set ef_construction to 64 or 128 to ensure the graph is robust enough for broad reverse-search queries. These settings keep query times sub-10ms even with hundreds of thousands of preference vectors.

The Cold Index Problem

Avoid latency spikes after database reboots by using the pg_prewarm extension. This loads the HNSW graph into the buffer cache immediately, ensuring the system remains "instant" from the first message. Monitor the Buffer Cache Hit Ratio to keep it near 100%.

Troubleshooting and Preemptive Maintenance

Upgrading embedding models causes "embedding drift," rendering old vectors obsolete. Use version-stamped migration scripts to re-vectorize user_interests in chunks while keeping legacy vectors active to avoid service blackouts. If Kafka lag spikes, offload vectorization to side-car workers using queues.

// In your Kafka Consumer:
ProcessSemanticMatch::dispatch($incomingEvent)->onQueue('high-speed');

Monitor your HNSW index health. As data grows, fragmenting can occur. Use REINDEX INDEX CONCURRENTLY idx_user_interests_vector; to rebuild the index without locking tables. This maintains performance even as you scale to millions of intent profiles.

Shipping without the Shrapnel

Semantic alerting moves us from static tables to event-driven reality. By using pgvector for reverse queries and Kafka for the backbone, we identify signal in milliseconds. Decoupling these components protects your core application from third-party API failures or rate limits, as seen in complex SaaS architectures where message brokers isolate throttling issues.

In 2026, building beyond the chatbot means treating user intent as the index. automated curation via semantic filters allows systems to provide only the notifications that truly matter. Keep your embeddings sharp and your queues buffered to avoid the "Update Storm" and deliver high-signal intelligence.

Sources & Further Reading

Frequently Asked Questions

What is the core difference between standard RAG and push-based semantic alerting?

Standard RAG is a pull-based system where a user query fetches context from a database. In contrast, push-based semantic alerting is a reverse search pattern. Here, user interests are stored as static intent vectors in a database like pgvector, and incoming data streams act as the query. The system proactively identifies matches and notifies relevant users, making it ideal for high-scale, real-time monitoring environments.

How does the HNSW index improve semantic alerting performance?

HNSW (Hierarchical Navigable Small Worlds) indexes allow for efficient similarity searches by creating a graph-based structure of vectors. Unlike IVFFlat, HNSW doesn't require a training step and maintains high accuracy even as user intent data shifts. By tuning parameters like m and ef_construction, developers can achieve sub-10ms query times across hundreds of thousands of vectors, preventing the Kafka consumer from lagging during high-throughput events.

Share this article

Related Articles

Discussion

No comments yet — be the first to share your thoughts.

Leave a comment

Comments are moderated before appearing.

Max 2,000 characters · not published

We respect your privacy