Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
supabase.py500 linesDownload Raw Back to vectorstores
1from __future__ import annotations2 3import uuid4import warnings5from itertools import repeat6from typing import (7    TYPE_CHECKING,8    Any,9    Dict,10    Iterable,11    List,12    Optional,13    Tuple,14    Type,15    Union,16)17 18import numpy as np19from langchain_core.documents import Document20from langchain_core.embeddings import Embeddings21from langchain_core.vectorstores import VectorStore22 23from langchain_community.vectorstores.utils import maximal_marginal_relevance24 25if TYPE_CHECKING:26    import supabase27 28 29class SupabaseVectorStore(VectorStore):30    """`Supabase Postgres` vector store.31 32    It assumes you have the `pgvector`33    extension installed and a `match_documents` (or similar) function. For more details:34    https://integrations.langchain.com/vectorstores?integration_name=SupabaseVectorStore35 36    You can implement your own `match_documents` function in order to limit the search37    space to a subset of documents based on your own authorization or business logic.38 39    Note that the Supabase Python client does not yet support async operations.40 41    If you'd like to use `max_marginal_relevance_search`, please review the instructions42    below on modifying the `match_documents` function to return matched embeddings.43 44 45    Examples:46 47    .. code-block:: python48 49        from langchain_community.embeddings.openai import OpenAIEmbeddings50        from langchain_core.documents import Document51        from langchain_community.vectorstores import SupabaseVectorStore52        from supabase.client import create_client53 54        docs = [55            Document(page_content="foo", metadata={"id": 1}),56        ]57        embeddings = OpenAIEmbeddings()58        supabase_client = create_client("my_supabase_url", "my_supabase_key")59        vector_store = SupabaseVectorStore.from_documents(60            docs,61            embeddings,62            client=supabase_client,63            table_name="documents",64            query_name="match_documents",65            chunk_size=500,66        )67 68    To load from an existing table:69 70    .. code-block:: python71 72        from langchain_community.embeddings.openai import OpenAIEmbeddings73        from langchain_community.vectorstores import SupabaseVectorStore74        from supabase.client import create_client75 76 77        embeddings = OpenAIEmbeddings()78        supabase_client = create_client("my_supabase_url", "my_supabase_key")79        vector_store = SupabaseVectorStore(80            client=supabase_client,81            embedding=embeddings,82            table_name="documents",83            query_name="match_documents",84        )85 86    """87 88    def __init__(89        self,90        client: supabase.client.Client,91        embedding: Embeddings,92        table_name: str,93        chunk_size: int = 500,94        query_name: Union[str, None] = None,95    ) -> None:96        """Initialize with supabase client."""97        try:98            import supabase  # noqa: F40199        except ImportError:100            raise ImportError(101                "Could not import supabase python package. "102                "Please install it with `pip install supabase`."103            )104 105        self._client = client106        self._embedding: Embeddings = embedding107        self.table_name = table_name or "documents"108        self.query_name = query_name or "match_documents"109        self.chunk_size = chunk_size or 500110        # According to the SupabaseVectorStore JS implementation, the best chunk size111        # is 500. Though for large datasets it can be too large so it is configurable.112 113    @property114    def embeddings(self) -> Embeddings:115        return self._embedding116 117    def add_texts(118        self,119        texts: Iterable[str],120        metadatas: Optional[List[Dict[Any, Any]]] = None,121        ids: Optional[List[str]] = None,122        **kwargs: Any,123    ) -> List[str]:124        ids = ids or [str(uuid.uuid4()) for _ in texts]125        docs = self._texts_to_documents(texts, metadatas)126 127        vectors = self._embedding.embed_documents(list(texts))128        return self.add_vectors(vectors, docs, ids)129 130    @classmethod131    def from_texts(132        cls: Type["SupabaseVectorStore"],133        texts: List[str],134        embedding: Embeddings,135        metadatas: Optional[List[dict]] = None,136        client: Optional[supabase.client.Client] = None,137        table_name: Optional[str] = "documents",138        query_name: Union[str, None] = "match_documents",139        chunk_size: int = 500,140        ids: Optional[List[str]] = None,141        **kwargs: Any,142    ) -> "SupabaseVectorStore":143        """Return VectorStore initialized from texts and embeddings."""144 145        if not client:146            raise ValueError("Supabase client is required.")147 148        if not table_name:149            raise ValueError("Supabase document table_name is required.")150 151        embeddings = embedding.embed_documents(texts)152        ids = [str(uuid.uuid4()) for _ in texts]153        docs = cls._texts_to_documents(texts, metadatas)154        cls._add_vectors(155            client, table_name, embeddings, docs, ids, chunk_size, **kwargs156        )157 158        return cls(159            client=client,160            embedding=embedding,161            table_name=table_name,162            query_name=query_name,163            chunk_size=chunk_size,164        )165 166    def add_vectors(167        self,168        vectors: List[List[float]],169        documents: List[Document],170        ids: List[str],171    ) -> List[str]:172        return self._add_vectors(173            self._client, self.table_name, vectors, documents, ids, self.chunk_size174        )175 176    def similarity_search(177        self,178        query: str,179        k: int = 4,180        filter: Optional[Dict[str, Any]] = None,181        **kwargs: Any,182    ) -> List[Document]:183        vector = self._embedding.embed_query(query)184        return self.similarity_search_by_vector(vector, k=k, filter=filter, **kwargs)185 186    def similarity_search_by_vector(187        self,188        embedding: List[float],189        k: int = 4,190        filter: Optional[Dict[str, Any]] = None,191        **kwargs: Any,192    ) -> List[Document]:193        result = self.similarity_search_by_vector_with_relevance_scores(194            embedding, k=k, filter=filter, **kwargs195        )196 197        documents = [doc for doc, _ in result]198 199        return documents200 201    def similarity_search_with_relevance_scores(202        self,203        query: str,204        k: int = 4,205        filter: Optional[Dict[str, Any]] = None,206        **kwargs: Any,207    ) -> List[Tuple[Document, float]]:208        vector = self._embedding.embed_query(query)209        return self.similarity_search_by_vector_with_relevance_scores(210            vector, k=k, filter=filter, **kwargs211        )212 213    def match_args(214        self, query: List[float], filter: Optional[Dict[str, Any]]215    ) -> Dict[str, Any]:216        ret: Dict[str, Any] = dict(query_embedding=query)217        if filter:218            ret["filter"] = filter219        return ret220 221    def similarity_search_by_vector_with_relevance_scores(222        self,223        query: List[float],224        k: int,225        filter: Optional[Dict[str, Any]] = None,226        postgrest_filter: Optional[str] = None,227        score_threshold: Optional[float] = None,228    ) -> List[Tuple[Document, float]]:229        # Convert MongoDB-style filter to PostgreSQL syntax if needed230        if filter:231            for key, value in filter.items():232                if isinstance(value, dict) and "$in" in value:233                    # Extract the list of values for the $in operator234                    in_values = value["$in"]235                    # Create a PostgreSQL IN clause236                    values_str = ",".join(f"'{str(v)}'" for v in in_values)237                    new_filter = f"metadata->>{key} IN ({values_str})"238 239                    # Combine with existing postgrest_filter if present240                    if postgrest_filter:241                        postgrest_filter = f"({postgrest_filter}) and ({new_filter})"242                    else:243                        postgrest_filter = new_filter244 245        match_documents_params = self.match_args(query, filter)246        query_builder = self._client.rpc(self.query_name, match_documents_params)247 248        if postgrest_filter:249            query_builder.params = query_builder.params.set(250                "and", f"({postgrest_filter})"251            )252 253        query_builder.params = query_builder.params.set("limit", k)254 255        res = query_builder.execute()256 257        match_result = [258            (259                Document(260                    metadata=search.get("metadata", {}),261                    page_content=search.get("content", ""),262                ),263                search.get("similarity", 0.0),264            )265            for search in res.data266            if search.get("content")267        ]268 269        if score_threshold is not None:270            match_result = [271                (doc, similarity)272                for doc, similarity in match_result273                if similarity >= score_threshold274            ]275            if len(match_result) == 0:276                warnings.warn(277                    "No relevant docs were retrieved using the relevance score"278                    f" threshold {score_threshold}"279                )280 281        return match_result282 283    def similarity_search_by_vector_returning_embeddings(284        self,285        query: List[float],286        k: int,287        filter: Optional[Dict[str, Any]] = None,288        postgrest_filter: Optional[str] = None,289    ) -> List[Tuple[Document, float, np.ndarray]]:290        match_documents_params = self.match_args(query, filter)291        query_builder = self._client.rpc(self.query_name, match_documents_params)292 293        if postgrest_filter:294            query_builder.params = query_builder.params.set(295                "and", f"({postgrest_filter})"296            )297 298        query_builder.params = query_builder.params.set("limit", k)299 300        res = query_builder.execute()301 302        match_result = [303            (304                Document(305                    metadata=search.get("metadata", {}),306                    page_content=search.get("content", ""),307                ),308                search.get("similarity", 0.0),309                # Supabase returns a vector type as its string represation (!).310                # This is a hack to convert the string to numpy array.311                np.fromstring(312                    search.get("embedding", "").strip("[]"), np.float32, sep=","313                ),314            )315            for search in res.data316            if search.get("content")317        ]318 319        return match_result320 321    @staticmethod322    def _texts_to_documents(323        texts: Iterable[str],324        metadatas: Optional[Iterable[Dict[Any, Any]]] = None,325    ) -> List[Document]:326        """Return list of Documents from list of texts and metadatas."""327        if metadatas is None:328            metadatas = repeat({})329 330        docs = [331            Document(page_content=text, metadata=metadata)332            for text, metadata in zip(texts, metadatas)333        ]334 335        return docs336 337    @staticmethod338    def _add_vectors(339        client: supabase.client.Client,340        table_name: str,341        vectors: List[List[float]],342        documents: List[Document],343        ids: List[str],344        chunk_size: int,345        **kwargs: Any,346    ) -> List[str]:347        """Add vectors to Supabase table."""348 349        rows: List[Dict[str, Any]] = [350            {351                "id": ids[idx],352                "content": documents[idx].page_content,353                "embedding": embedding,354                "metadata": documents[idx].metadata,355                **kwargs,356            }357            for idx, embedding in enumerate(vectors)358        ]359        id_list: List[str] = []360        for i in range(0, len(rows), chunk_size):361            chunk = rows[i : i + chunk_size]362 363            result = client.from_(table_name).upsert(chunk).execute()364 365            if len(result.data) == 0:366                raise Exception("Error inserting: No rows added")367 368            # VectorStore.add_vectors returns ids as strings369            ids = [str(i.get("id")) for i in result.data if i.get("id")]370 371            id_list.extend(ids)372 373        return id_list374 375    def max_marginal_relevance_search_by_vector(376        self,377        embedding: List[float],378        k: int = 4,379        fetch_k: int = 20,380        lambda_mult: float = 0.5,381        **kwargs: Any,382    ) -> List[Document]:383        """Return docs selected using the maximal marginal relevance.384 385        Maximal marginal relevance optimizes for similarity to query AND diversity386        among selected documents.387 388        Args:389            embedding: Embedding to look up documents similar to.390            k: Number of Documents to return. Defaults to 4.391            fetch_k: Number of Documents to fetch to pass to MMR algorithm.392            lambda_mult: Number between 0 and 1 that determines the degree393                        of diversity among the results with 0 corresponding394                        to maximum diversity and 1 to minimum diversity.395                        Defaults to 0.5.396        Returns:397            List of Documents selected by maximal marginal relevance.398        """399        result = self.similarity_search_by_vector_returning_embeddings(400            embedding, fetch_k401        )402 403        matched_documents = [doc_tuple[0] for doc_tuple in result]404        matched_embeddings = [doc_tuple[2] for doc_tuple in result]405 406        mmr_selected = maximal_marginal_relevance(407            np.array([embedding], dtype=np.float32),408            matched_embeddings,409            k=k,410            lambda_mult=lambda_mult,411        )412 413        filtered_documents = [matched_documents[i] for i in mmr_selected]414 415        return filtered_documents416 417    def max_marginal_relevance_search(418        self,419        query: str,420        k: int = 4,421        fetch_k: int = 20,422        lambda_mult: float = 0.5,423        **kwargs: Any,424    ) -> List[Document]:425        """Return docs selected using the maximal marginal relevance.426 427        Maximal marginal relevance optimizes for similarity to query AND diversity428        among selected documents.429 430        Args:431            query: Text to look up documents similar to.432            k: Number of Documents to return. Defaults to 4.433            fetch_k: Number of Documents to fetch to pass to MMR algorithm.434            lambda_mult: Number between 0 and 1 that determines the degree435                        of diversity among the results with 0 corresponding436                        to maximum diversity and 1 to minimum diversity.437                        Defaults to 0.5.438        Returns:439            List of Documents selected by maximal marginal relevance.440 441        `max_marginal_relevance_search` requires that `query_name` returns matched442        embeddings alongside the match documents. The following function443        demonstrates how to do this:444 445        ```sql446        CREATE FUNCTION match_documents_embeddings(query_embedding vector(1536),447                                                   match_count int)448            RETURNS TABLE(449                id uuid,450                content text,451                metadata jsonb,452                embedding vector(1536),453                similarity float)454            LANGUAGE plpgsql455            AS $$456            # variable_conflict use_column457        BEGIN458            RETURN query459            SELECT460                id,461                content,462                metadata,463                embedding,464                1 -(docstore.embedding <=> query_embedding) AS similarity465            FROM466                docstore467            ORDER BY468                docstore.embedding <=> query_embedding469            LIMIT match_count;470        END;471        $$;472        ```473        """474        embedding = self._embedding.embed_query(query)475        docs = self.max_marginal_relevance_search_by_vector(476            embedding, k, fetch_k, lambda_mult=lambda_mult477        )478        return docs479 480    def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> None:481        """Delete by vector IDs.482 483        Args:484            ids: List of ids to delete.485        """486 487        if ids is None:488            raise ValueError("No ids provided to delete.")489 490        rows: List[Dict[str, Any]] = [491            {492                "id": id,493            }494            for id in ids495        ]496 497        # TODO: Check if this can be done in bulk498        for row in rows:499            self._client.from_(self.table_name).delete().eq("id", row["id"]).execute()500 
codekingpro/portable-devtools · Team Ai