Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

feat(retrieval): DB-native RRF via Supabase RPC, drop Python-side fusion - #4

Merged
johansabent merged 3 commits into
mainfrom
feat/phase3-hybrid-search-rrf
Mar 18, 2026
Merged

feat(retrieval): DB-native RRF via Supabase RPC, drop Python-side fusion#4
johansabent merged 3 commits into
mainfrom
feat/phase3-hybrid-search-rrf

Conversation

@johansabent

Copy link
Copy Markdown
Owner

Summary

  • New hybrid_search_rrf() Postgres RPC that fuses dense cosine vector search with BM25-style full-text search using Reciprocal Rank Fusion — all inside the DB, only top-K rows cross the network
  • Rewrote query.py to use asyncpg connection pool + direct RPC call, replacing the LlamaIndex VectorStoreIndex retriever. LlamaIndex retained only for embedding generation (GoogleGenAIEmbedding) and LLM calls (GoogleGenAI)
  • New SQL migrations: 002_pg_trgm.sql (trigram extension) and 003_hybrid_search_rrf.sql (RPC function with parameterized weights, over-fetching, and metadata stripping)
  • Added requirements.txt with asyncpg>=0.29.0
  • Removed SIMILARITY_CUTOFF (RRF scores are relative, not cosine distances)

Phase 3 of 4

This implements Mandate 1 (DB-Native Hybrid Search) from SPRINT_PLAN.md. Phases 1 (Foundation) and 2 (Incremental Ingestion) are already merged.

Test plan

  • Run migrations against local Supabase: psql ... -f migrations/002_pg_trgm.sql -f migrations/003_hybrid_search_rrf.sql
  • python query.py "What is the gateway?" — returns answer with sources and RRF scores
  • python query.py "northflank" — keyword-exact query ranks relevant docs higher than Phase 2 baseline
  • LOG_LEVEL=DEBUG python query.py "..." — shows embedding, RPC, and LLM steps
  • CI py_compile passes on Python 3.10 + 3.11
  • No UserWarning about missing covering index (HNSW from Phase 1 covers dense path; we bypass vecs query entirely)

🤖 Generated with Claude Code

Replace the LlamaIndex VectorStoreIndex retriever in query.py with a
direct asyncpg call to a new hybrid_search_rrf() Postgres RPC function.
The RPC fuses dense cosine vector search with BM25-style full-text search
using Reciprocal Rank Fusion — all inside the database, so only the final
top-K rows cross the network.

Key changes:
- New migration 002: pg_trgm extension
- New migration 003: hybrid_search_rrf() RPC function
- query.py: async rewrite with asyncpg connection pool, direct embedding
  generation via GoogleGenAIEmbedding, and LLM call via GoogleGenAI
- Removed SIMILARITY_CUTOFF (RRF scores are relative, not cosine)
- Added requirements.txt with asyncpg>=0.29.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 18, 2026 01:58
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the retrieval-augmented generation (RAG) system by implementing a database-native hybrid search mechanism. By moving the complex fusion logic for vector and full-text search into a PostgreSQL RPC function, the system reduces data transfer overhead and leverages the database's efficiency. This change streamlines the query process, making it more performant and robust, while maintaining the flexibility of using LlamaIndex for embedding and LLM interactions.

Highlights

  • DB-Native Hybrid Search: Introduced a new hybrid_search_rrf() Postgres RPC function that performs Reciprocal Rank Fusion (RRF) of dense cosine vector search and BM25-style full-text search directly within the database, ensuring only top-K results are returned over the network.
  • Query Engine Refactor: Rewrote query.py to utilize an asyncpg connection pool and direct RPC calls to the new hybrid_search_rrf() function, replacing the LlamaIndex VectorStoreIndex retriever. LlamaIndex is now primarily used for embedding generation and LLM calls.
  • Database Migrations: Added two new SQL migration files: 002_pg_trgm.sql to enable the pg_trgm extension for fuzzy text matching, and 003_hybrid_search_rrf.sql to create the RRF RPC function.
  • Dependency Update: Added asyncpg>=0.29.0 to requirements.txt to support asynchronous PostgreSQL interactions.
  • Configuration Simplification: Removed the SIMILARITY_CUTOFF environment variable as RRF scores are relative and do not rely on a fixed similarity threshold.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant architectural improvement by moving the hybrid search and fusion logic into a PostgreSQL RPC function. This reduces data transfer and leverages the database's strengths. The refactoring of query.py to use asyncpg is clean and aligns with this new approach.

My review includes two main points:

  1. A critical security fix to prevent potential SQL injection in query.py by using parameterized queries correctly for the embedding vector.
  2. A high-impact performance suggestion for the hybrid_search_rrf SQL function to create a GIN index, which will substantially speed up full-text search operations.

After addressing these points, this will be an excellent enhancement to the RAG system's retrieval performance and security.

Comment thread rag-agent/query.py
Comment on lines +77 to +87
embedding_str = "[" + ",".join(str(v) for v in query_embedding) + "]"
log.debug("Calling hybrid_search_rrf (match_count=%d) ...", top_k)

log.info("Question: %s", question)
log.debug("Searching vector store (top_k=%d, cutoff=%.2f) ...", top_k, similarity_cutoff)
try:
rows = await pool.fetch(
"SELECT id, content, metadata, score "
"FROM hybrid_search_rrf($1, $2::vector(3072), $3)",
question,
embedding_str,
top_k,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

There is a critical SQL injection vulnerability here. The embedding vector is being converted to a string and then used in the SQL query. This is unsafe. You should pass the query_embedding list directly as a parameter to pool.fetch and let asyncpg handle the safe serialization. This prevents any possibility of SQL injection through the embedding data.

Here's how to fix it:

    log.debug("Calling hybrid_search_rrf (match_count=%d) ...", top_k)

    try:
        rows = await pool.fetch(
            "SELECT id, content, metadata, score "
            "FROM hybrid_search_rrf($1, $2::vector(3072), $3)",
            question,
            query_embedding,
            top_k,
        )

Comment on lines +49 to +63
fulltext AS (
SELECT
f.id,
ROW_NUMBER() OVER (
ORDER BY ts_rank_cd(
to_tsvector('english', f.metadata ->> 'text'),
websearch_to_tsquery('english', query_text)
) DESC
) AS rank_ix
FROM vecs.openclaw_docs f
WHERE
f.metadata ->> 'text' IS NOT NULL
AND to_tsvector('english', f.metadata ->> 'text')
@@ websearch_to_tsquery('english', query_text)
LIMIT (match_count * 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The full-text search portion of this query is inefficient because it computes to_tsvector on the fly for every row during every query execution. This will not scale well with a larger number of documents.

To significantly improve performance, you should create a GIN index on the tsvector expression. This will allow PostgreSQL to use an index for the full-text search operator (@@), making it much faster.

I recommend creating a new migration file (e.g., 004_fts_index.sql) with the following content:

CREATE INDEX IF NOT EXISTS openclaw_docs_fts_idx
ON vecs.openclaw_docs
USING GIN (to_tsvector('english', metadata ->> 'text'));

This is a non-destructive operation that will dramatically speed up your hybrid search.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR shifts the RAG query path from LlamaIndex’s SupabaseVectorStore query engine to a DB-native hybrid search RPC (RRF fusion of vector + full-text), and documents the required Postgres migrations to support it.

Changes:

  • Replace rag-agent/query.py retrieval with asyncpg + hybrid_search_rrf() RPC and keep prompt-injection guardrails + source attribution.
  • Add SQL migrations/docs for enabling full-text support and creating the hybrid search function.
  • Update setup documentation to include running migrations and add asyncpg to Python deps.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Adds a setup step for running DB migrations after initial ingestion.
rag-agent/requirements.txt Introduces an explicit Python dependency file (currently only asyncpg).
rag-agent/query.py Switches query-time retrieval to asyncpg RPC-based hybrid search + builds LLM prompt manually.
rag-agent/migrations/README.md Documents migration purpose/order and how to run via psql.
rag-agent/migrations/002_pg_trgm.sql Enables pg_trgm extension.
rag-agent/migrations/003_hybrid_search_rrf.sql Adds hybrid_search_rrf() SQL function for fused retrieval.
rag-agent/.env.example Updates query config notes to reflect hybrid RPC usage.
CLAUDE.md Adds repo-specific workflow/architecture guidance for Claude Code.
Comments suppressed due to low confidence (1)

rag-agent/query.py:56

  • DB_CONNECTION_STRING is read but never validated. If it is unset/empty, _get_pool() will raise when asyncpg.create_pool() is called; add an explicit check (similar to GOOGLE_API_KEY) and fail with a clear error message.
    api_key = os.getenv("GOOGLE_API_KEY")
    db_connection = os.getenv("DB_CONNECTION_STRING")

    if not api_key or api_key == "YOUR_API_KEY_HERE":
        log.error("GOOGLE_API_KEY not set in .env")
        return

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@@ -0,0 +1 @@
asyncpg>=0.29.0
Comment thread rag-agent/query.py
Comment on lines +82 to +83
"SELECT id, content, metadata, score "
"FROM hybrid_search_rrf($1, $2::vector(3072), $3)",
@@ -0,0 +1,88 @@
-- Migration 003: Create hybrid_search_rrf() RPC function.
--
-- Run this ONCE in the Supabase SQL editor (or via psql) AFTER migrations 001 and 002.
johansabent and others added 2 commits March 18, 2026 01:41
The vecs library stores node text inside metadata->_node_content as a
serialized JSON string, not directly in metadata->text. Update the SQL
RPC and comments accordingly.

Switch query.py to async equivalents (aget_query_embedding, acomplete)
so the event loop is not blocked during embedding and LLM generation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Complete requirements.txt with all direct Python dependencies
- Bump package.json version to 0.1.0-beta
- Gitignore .claude/ and root tools/ (personal utilities)
- Remove corpus CHANGELOG.md from repo root (not a project changelog)
- Update README: remove CHANGELOG ref, add pip install step, add Known Limitations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@johansabent
johansabent merged commit c7b5f62 into main Mar 18, 2026
2 checks passed
@johansabent
johansabent deleted the feat/phase3-hybrid-search-rrf branch March 18, 2026 13:14
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants