SupabaseHybridSearch as in langchainjs
Labels: name: area: embeddings, name: auto:enhancement

### Feature request

Hybrid search for Supabase in the python version of Langchain would be much appreciated:

[Supabase Hybrid Search](https://js.langchain.com/docs/modules/data_connection/retrievers/integrations/supabase-hybrid/)

[langchain/src/retrievers/supabase.ts](https://github.com/hwchase17/langchainjs/blob/main/langchain/src/retrievers/supabase.ts)



### Motivation

Better retrieval performance

### Your contribution

I've done some work using [langchain.schema.retriever.BaseRetriever](https://api.python.langchain.com/en/latest/schema/langchain.schema.retriever.BaseRetriever.html#langchain.schema.retriever.BaseRetriever) as a template, but it's not working and I'm not sure i'll have time to figure it out:

```
from pydantic import BaseModel, Field
from supabase.client import Client, create_client
from langchain.embeddings.base import Embeddings
from langchain.docstore.document import Document
from langchain.schema.retriever import BaseRetriever
from langchain.callbacks.manager import Callbacks, CallbackManagerForRetrieverRun
from pydantic import BaseModel, Field, validator

from dataclasses import dataclass
from typing import List, Dict, Optional, Any
import asyncio

class SearchEmbeddingsParams:
    def __init__(self, query_embedding, match_count):
        self.query_embedding = query_embedding
        self.match_count = match_count

class SearchKeywordParams:
    def __init__(self, query_text, match_count):
        self.query_text = query_text
        self.match_count = match_count

class SearchResponseRow:
    def __init__(self, id, content, metadata, similarity):
        self.id = id
        self.content = content
        self.metadata = metadata
        self.similarity = similarity

class SearchResult:
    def __init__(self, document, number1, number2):
        self.document = document
        self.number1 = number1
        self.number2 = number2


class SupabaseHybridSearch(BaseRetriever, BaseModel):

    embeddings: Any
    client: Client
    table_name: str = "documents"
    similarity_query_name: str = "match_documents"
    keyword_query_name: str = "kw_match_documents"
    similarity_k: int = 2
    keyword_k: int = 2

    class Config:
        arbitrary_types_allowed = True

    async def similarity_search(self, query, k, _callbacks=None):
        # embedded_query = await self.embeddings.embed_query(query)
        embedded_query = self.embeddings.embed_query(query)
        match_documents_params = SearchEmbeddingsParams(embedded_query, k)
        res = self.client.rpc(self.similarity_query_name, match_documents_params.__dict__).execute()
        searches, error = self.client.rpc(self.similarity_query_name, match_documents_params)

        if error:
            raise Exception(f"Error searching for documents: {error.code} {error.message} {error.details}")

        return [SearchResult(Document(metadata=resp.metadata, pageContent=resp.content), 
                resp.similarity, resp.id) for resp in searches]

    async def keyword_search(self, query, k):
        kw_match_documents_params = SearchKeywordParams(query, k)
        searches, error = await self.client.rpc(self.keyword_query_name, kw_match_documents_params)

        if error:
            raise Exception(f"Error searching for documents: {error.code} {error.message} {error.details}")

        return [SearchResult(Document(metadata=resp.metadata, pageContent=resp.content), 
                resp.similarity * 10, resp.id) for resp in searches]

    async def hybrid_search(self, query, similarity_k, keyword_k, callbacks=None):
        similarity_search = self.similarity_search(query, similarity_k, callbacks)

        similarity_search = self.similarity_search(query, similarity_k, callbacks)
        keyword_search = self.keyword_search(query, keyword_k)

        results = [result for sublist in await asyncio.gather(similarity_search, keyword_search) for result in sublist]

        picks = {}
        for result in results:
            id, nextScore = result.number2, result.number1
            prevScore = picks.get(id)

            if prevScore is None or nextScore > prevScore:
                picks[id] = result

        return sorted(picks.values(), key=lambda result: result.number1, reverse=True)

    def _get_relevant_documents(self, query: str) -> List[Document]:
        raise NotImplementedError

    async def _aget_relevant_documents(self, query: str) -> List[Document]:
        searchResults = await self.hybrid_search(query, self.similarity_k, self.keyword_k, None)
        return [result.document for result in searchResults]
```