Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
pgvector.py1388 linesDownload Raw Back to vectorstores
1from __future__ import annotations2 3import contextlib4import enum5import json6import logging7import uuid8from typing import (9    Any,10    Callable,11    Dict,12    Generator,13    Iterable,14    List,15    Mapping,16    Optional,17    Tuple,18    Type,19    Union,20)21 22import numpy as np23import sqlalchemy24from langchain_core._api import deprecated, warn_deprecated25from sqlalchemy import delete, func26from sqlalchemy.dialects.postgresql import JSON, JSONB, UUID27from sqlalchemy.orm import Session, relationship28 29try:30    from sqlalchemy.orm import declarative_base31except ImportError:32    from sqlalchemy.ext.declarative import declarative_base33 34try:35    from sqlalchemy import SQLColumnExpression36except ImportError:37    # for sqlalchemy < 238    SQLColumnExpression = Any  # type: ignore[assignment,misc]39 40from langchain_core.documents import Document41from langchain_core.embeddings import Embeddings42from langchain_core.runnables.config import run_in_executor43from langchain_core.utils import get_from_dict_or_env44from langchain_core.vectorstores import VectorStore45 46from langchain_community.vectorstores.utils import maximal_marginal_relevance47 48 49class DistanceStrategy(str, enum.Enum):50    """Enumerator of the Distance strategies."""51 52    EUCLIDEAN = "l2"53    COSINE = "cosine"54    MAX_INNER_PRODUCT = "inner"55 56 57DEFAULT_DISTANCE_STRATEGY = DistanceStrategy.COSINE58 59Base = declarative_base()  # type: Any60 61 62_LANGCHAIN_DEFAULT_COLLECTION_NAME = "langchain"63 64 65class BaseModel(Base):66    """Base model for the SQL stores."""67 68    __abstract__ = True69    uuid = sqlalchemy.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)70 71 72_classes: Any = None73 74COMPARISONS_TO_NATIVE = {75    "$eq": "==",76    "$ne": "!=",77    "$lt": "<",78    "$lte": "<=",79    "$gt": ">",80    "$gte": ">=",81}82 83SPECIAL_CASED_OPERATORS = {84    "$in",85    "$nin",86    "$between",87}88 89TEXT_OPERATORS = {90    "$like",91    "$ilike",92}93 94LOGICAL_OPERATORS = {"$and", "$or"}95 96SUPPORTED_OPERATORS = (97    set(COMPARISONS_TO_NATIVE)98    .union(TEXT_OPERATORS)99    .union(LOGICAL_OPERATORS)100    .union(SPECIAL_CASED_OPERATORS)101)102 103 104def _get_embedding_collection_store(105    vector_dimension: Optional[int] = None, *, use_jsonb: bool = True106) -> Any:107    global _classes108    if _classes is not None:109        return _classes110 111    from pgvector.sqlalchemy import Vector112 113    class CollectionStore(BaseModel):114        """Collection store."""115 116        __tablename__ = "langchain_pg_collection"117 118        name = sqlalchemy.Column(sqlalchemy.String)119        cmetadata = sqlalchemy.Column(JSON)120 121        embeddings = relationship(122            "EmbeddingStore",123            back_populates="collection",124            passive_deletes=True,125        )126 127        @classmethod128        def get_by_name(129            cls, session: Session, name: str130        ) -> Optional["CollectionStore"]:131            return session.query(cls).filter(cls.name == name).first()132 133        @classmethod134        def get_or_create(135            cls,136            session: Session,137            name: str,138            cmetadata: Optional[dict] = None,139        ) -> Tuple["CollectionStore", bool]:140            """141            Get or create a collection.142            Returns [Collection, bool] where the bool is True if the collection was created.143            """  # noqa: E501144            created = False145            collection = cls.get_by_name(session, name)146            if collection:147                return collection, created148 149            collection = cls(name=name, cmetadata=cmetadata)150            session.add(collection)151            session.commit()152            created = True153            return collection, created154 155    if use_jsonb:156        # TODO(PRIOR TO LANDING): Create a gin index on the cmetadata field157        class EmbeddingStore(BaseModel):158            """Embedding store."""159 160            __tablename__ = "langchain_pg_embedding"161 162            collection_id = sqlalchemy.Column(163                UUID(as_uuid=True),164                sqlalchemy.ForeignKey(165                    f"{CollectionStore.__tablename__}.uuid",166                    ondelete="CASCADE",167                ),168            )169            collection = relationship(CollectionStore, back_populates="embeddings")170 171            embedding: Vector = sqlalchemy.Column(Vector(vector_dimension))172            document = sqlalchemy.Column(sqlalchemy.String, nullable=True)173            cmetadata = sqlalchemy.Column(JSONB, nullable=True)174 175            # custom_id : any user defined id176            custom_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)177 178            __table_args__ = (179                sqlalchemy.Index(180                    "ix_cmetadata_gin",181                    "cmetadata",182                    postgresql_using="gin",183                    postgresql_ops={"cmetadata": "jsonb_path_ops"},184                ),185            )186 187    else:188        # For backwards comaptibilty with older versions of pgvector189        # This should be removed in the future (remove during migration)190        class EmbeddingStore(BaseModel):  # type: ignore[no-redef]191            """Embedding store."""192 193            __tablename__ = "langchain_pg_embedding"194 195            collection_id = sqlalchemy.Column(196                UUID(as_uuid=True),197                sqlalchemy.ForeignKey(198                    f"{CollectionStore.__tablename__}.uuid",199                    ondelete="CASCADE",200                ),201            )202            collection = relationship(CollectionStore, back_populates="embeddings")203 204            embedding: Vector = sqlalchemy.Column(Vector(vector_dimension))205            document = sqlalchemy.Column(sqlalchemy.String, nullable=True)206            cmetadata = sqlalchemy.Column(JSON, nullable=True)207 208            # custom_id : any user defined id209            custom_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)210 211    _classes = (EmbeddingStore, CollectionStore)212 213    return _classes214 215 216def _results_to_docs(docs_and_scores: Any) -> List[Document]:217    """Return docs from docs and scores."""218    return [doc for doc, _ in docs_and_scores]219 220 221@deprecated(222    since="0.0.31",223    message=(224        "This class is pending deprecation and may be removed in a future version. "225        "You can swap to using the `PGVector` "226        "implementation in `langchain_postgres`. "227        "Please read the guidelines in the doc-string of this class "228        "to follow prior to migrating as there are some differences "229        "between the implementations. "230        "See <https://github.com/langchain-ai/langchain-postgres> for details about "231        "the new implementation."232    ),233    alternative="from langchain_postgres import PGVector;",234    pending=True,235)236class PGVector(VectorStore):237    """`Postgres`/`PGVector` vector store.238 239    **DEPRECATED**: This class is pending deprecation and will likely receive240        no updates. An improved version of this class is available in241        `langchain_postgres` as `PGVector`. Please use that class instead.242 243        When migrating please keep in mind that:244            * The new implementation works with psycopg3, not with psycopg2245              (This implementation does not work with psycopg3).246            * Filtering syntax has changed to use $ prefixed operators for JSONB247              metadata fields. (New implementation only uses JSONB field for metadata)248            * The new implementation made some schema changes to address issues249              with the existing implementation. So you will need to re-create250              your tables and re-index your data or else carry out a manual251              migration.252 253    To use, you should have the ``pgvector`` python package installed.254 255    Args:256        connection_string: Postgres connection string.257        embedding_function: Any embedding function implementing258            `langchain.embeddings.base.Embeddings` interface.259        embedding_length: The length of the embedding vector. (default: None)260            NOTE: This is not mandatory. Defining it will prevent vectors of261            any other size to be added to the embeddings table but, without it,262            the embeddings can't be indexed.263        collection_name: The name of the collection to use. (default: langchain)264            NOTE: This is not the name of the table, but the name of the collection.265            The tables will be created when initializing the store (if not exists)266            So, make sure the user has the right permissions to create tables.267        distance_strategy: The distance strategy to use. (default: COSINE)268        pre_delete_collection: If True, will delete the collection if it exists.269            (default: False). Useful for testing.270        engine_args: SQLAlchemy's create engine arguments.271        use_jsonb: Use JSONB instead of JSON for metadata. (default: True)272            Strongly discouraged from using JSON as it's not as efficient273            for querying.274            It's provided here for backwards compatibility with older versions,275            and will be removed in the future.276        create_extension: If True, will create the vector extension if it doesn't exist.277            disabling creation is useful when using ReadOnly Databases.278 279    Example:280 281       .. code-block:: python282 283           from langchain_community.vectorstores import PGVector284           from langchain_community.embeddings.openai import OpenAIEmbeddings285           CONNECTION_STRING = "postgresql+psycopg2://hwc@localhost:5432/test3"286           COLLECTION_NAME = "state_of_the_union_test"287           embeddings = OpenAIEmbeddings()288           vectorestore = PGVector.from_documents(289               embedding=embeddings,290               documents=docs,291               collection_name=COLLECTION_NAME,292               connection_string=CONNECTION_STRING,293               use_jsonb=True,294 295    """  # noqa: E501296 297    def __init__(298        self,299        connection_string: str,300        embedding_function: Embeddings,301        embedding_length: Optional[int] = None,302        collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME,303        collection_metadata: Optional[dict] = None,304        distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY,305        pre_delete_collection: bool = False,306        logger: Optional[logging.Logger] = None,307        relevance_score_fn: Optional[Callable[[float], float]] = None,308        *,309        connection: Optional[sqlalchemy.engine.Connection] = None,310        engine_args: Optional[dict[str, Any]] = None,311        use_jsonb: bool = False,312        create_extension: bool = True,313    ) -> None:314        """Initialize the PGVector store."""315        self.connection_string = connection_string316        self.embedding_function = embedding_function317        self._embedding_length = embedding_length318        self.collection_name = collection_name319        self.collection_metadata = collection_metadata320        self._distance_strategy = distance_strategy321        self.pre_delete_collection = pre_delete_collection322        self.logger = logger or logging.getLogger(__name__)323        self.override_relevance_score_fn = relevance_score_fn324        self.engine_args = engine_args or {}325        self._bind = connection if connection else self._create_engine()326        self.use_jsonb = use_jsonb327        self.create_extension = create_extension328 329        if not use_jsonb:330            # Replace with a deprecation warning.331            warn_deprecated(332                "0.0.29",333                pending=True,334                message=(335                    "Please use JSONB instead of JSON for metadata. "336                    "This change will allow for more efficient querying that "337                    "involves filtering based on metadata. "338                    "Please note that filtering operators have been changed "339                    "when using JSONB metadata to be prefixed with a $ sign "340                    "to avoid name collisions with columns. "341                    "If you're using an existing database, you will need to create a "342                    "db migration for your metadata column to be JSONB and update your "343                    "queries to use the new operators. "344                ),345                alternative=(346                    "Instantiate with use_jsonb=True to use JSONB instead "347                    "of JSON for metadata."348                ),349            )350        self.__post_init__()351 352    def __post_init__(353        self,354    ) -> None:355        """Initialize the store."""356        if self.create_extension:357            self.create_vector_extension()358 359        EmbeddingStore, CollectionStore = _get_embedding_collection_store(360            self._embedding_length, use_jsonb=self.use_jsonb361        )362        self.CollectionStore = CollectionStore363        self.EmbeddingStore = EmbeddingStore364        self.create_tables_if_not_exists()365        self.create_collection()366 367    def __del__(self) -> None:368        if isinstance(self._bind, sqlalchemy.engine.Connection):369            self._bind.close()370 371    @property372    def embeddings(self) -> Embeddings:373        return self.embedding_function374 375    def _create_engine(self) -> sqlalchemy.engine.Engine:376        return sqlalchemy.create_engine(url=self.connection_string, **self.engine_args)377 378    def create_vector_extension(self) -> None:379        try:380            with Session(self._bind) as session:381                # The advisor lock fixes issue arising from concurrent382                # creation of the vector extension.383                # https://github.com/langchain-ai/langchain/issues/12933384                # For more information see:385                # https://www.postgresql.org/docs/16/explicit-locking.html#ADVISORY-LOCKS386                statement = sqlalchemy.text(387                    "BEGIN;"388                    "SELECT pg_advisory_xact_lock(1573678846307946496);"389                    "CREATE EXTENSION IF NOT EXISTS vector;"390                    "COMMIT;"391                )392                session.execute(statement)393                session.commit()394        except Exception as e:395            raise Exception(f"Failed to create vector extension: {e}") from e396 397    def create_tables_if_not_exists(self) -> None:398        with Session(self._bind) as session, session.begin():399            Base.metadata.create_all(session.get_bind())400 401    def drop_tables(self) -> None:402        with Session(self._bind) as session, session.begin():403            Base.metadata.drop_all(session.get_bind())404 405    def create_collection(self) -> None:406        if self.pre_delete_collection:407            self.delete_collection()408        with Session(self._bind) as session:409            self.CollectionStore.get_or_create(410                session, self.collection_name, cmetadata=self.collection_metadata411            )412 413    def delete_collection(self) -> None:414        self.logger.debug("Trying to delete collection")415        with Session(self._bind) as session:416            collection = self.get_collection(session)417            if not collection:418                self.logger.warning("Collection not found")419                return420            session.delete(collection)421            session.commit()422 423    @contextlib.contextmanager424    def _make_session(self) -> Generator[Session, None, None]:425        """Create a context manager for the session, bind to _conn string."""426        yield Session(self._bind)427 428    def delete(429        self,430        ids: Optional[List[str]] = None,431        collection_only: bool = False,432        **kwargs: Any,433    ) -> None:434        """Delete vectors by ids or uuids.435 436        Args:437            ids: List of ids to delete.438            collection_only: Only delete ids in the collection.439        """440        with Session(self._bind) as session:441            if ids is not None:442                self.logger.debug(443                    "Trying to delete vectors by ids (represented by the model "444                    "using the custom ids field)"445                )446 447                stmt = delete(self.EmbeddingStore)448 449                if collection_only:450                    collection = self.get_collection(session)451                    if not collection:452                        self.logger.warning("Collection not found")453                        return454 455                    stmt = stmt.where(456                        self.EmbeddingStore.collection_id == collection.uuid457                    )458 459                stmt = stmt.where(self.EmbeddingStore.custom_id.in_(ids))460                session.execute(stmt)461            session.commit()462 463    def get_collection(self, session: Session) -> Any:464        return self.CollectionStore.get_by_name(session, self.collection_name)465 466    @classmethod467    def _from(468        cls,469        texts: List[str],470        embeddings: List[List[float]],471        embedding: Embeddings,472        metadatas: Optional[List[dict]] = None,473        ids: Optional[List[str]] = None,474        collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME,475        distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY,476        connection_string: Optional[str] = None,477        pre_delete_collection: bool = False,478        *,479        use_jsonb: bool = False,480        **kwargs: Any,481    ) -> PGVector:482        if ids is None:483            ids = [str(uuid.uuid4()) for _ in texts]484 485        if not metadatas:486            metadatas = [{} for _ in texts]487        if connection_string is None:488            connection_string = cls.get_connection_string(kwargs)489 490        store = cls(491            connection_string=connection_string,492            collection_name=collection_name,493            embedding_function=embedding,494            distance_strategy=distance_strategy,495            pre_delete_collection=pre_delete_collection,496            use_jsonb=use_jsonb,497            **kwargs,498        )499 500        store.add_embeddings(501            texts=texts, embeddings=embeddings, metadatas=metadatas, ids=ids, **kwargs502        )503 504        return store505 506    def add_embeddings(507        self,508        texts: Iterable[str],509        embeddings: List[List[float]],510        metadatas: Optional[List[dict]] = None,511        ids: Optional[List[str]] = None,512        **kwargs: Any,513    ) -> List[str]:514        """Add embeddings to the vectorstore.515 516        Args:517            texts: Iterable of strings to add to the vectorstore.518            embeddings: List of list of embedding vectors.519            metadatas: List of metadatas associated with the texts.520            kwargs: vectorstore specific parameters521        """522        if ids is None:523            ids = [str(uuid.uuid4()) for _ in texts]524 525        if not metadatas:526            metadatas = [{} for _ in texts]527 528        with Session(self._bind) as session:529            collection = self.get_collection(session)530            if not collection:531                raise ValueError("Collection not found")532            documents = []533            for text, metadata, embedding, id in zip(texts, metadatas, embeddings, ids):534                embedding_store = self.EmbeddingStore(535                    embedding=embedding,536                    document=text,537                    cmetadata=metadata,538                    custom_id=id,539                    collection_id=collection.uuid,540                )541                documents.append(embedding_store)542            session.bulk_save_objects(documents)543            session.commit()544 545        return ids546 547    def add_texts(548        self,549        texts: Iterable[str],550        metadatas: Optional[List[dict]] = None,551        ids: Optional[List[str]] = None,552        **kwargs: Any,553    ) -> List[str]:554        """Run more texts through the embeddings and add to the vectorstore.555 556        Args:557            texts: Iterable of strings to add to the vectorstore.558            metadatas: Optional list of metadatas associated with the texts.559            kwargs: vectorstore specific parameters560 561        Returns:562            List of ids from adding the texts into the vectorstore.563        """564        embeddings = self.embedding_function.embed_documents(list(texts))565        return self.add_embeddings(566            texts=texts, embeddings=embeddings, metadatas=metadatas, ids=ids, **kwargs567        )568 569    def similarity_search(570        self,571        query: str,572        k: int = 4,573        filter: Optional[dict] = None,574        **kwargs: Any,575    ) -> List[Document]:576        """Run similarity search with PGVector with distance.577 578        Args:579            query (str): Query text to search for.580            k (int): Number of results to return. Defaults to 4.581            filter (Optional[Dict[str, str]]): Filter by metadata. Defaults to None.582 583        Returns:584            List of Documents most similar to the query.585        """586        embedding = self.embedding_function.embed_query(text=query)587        return self.similarity_search_by_vector(588            embedding=embedding,589            k=k,590            filter=filter,591        )592 593    def similarity_search_with_score(594        self,595        query: str,596        k: int = 4,597        filter: Optional[dict] = None,598    ) -> List[Tuple[Document, float]]:599        """Return docs most similar to query.600 601        Args:602            query: Text to look up documents similar to.603            k: Number of Documents to return. Defaults to 4.604            filter (Optional[Dict[str, str]]): Filter by metadata. Defaults to None.605 606        Returns:607            List of Documents most similar to the query and score for each.608        """609        embedding = self.embedding_function.embed_query(query)610        docs = self.similarity_search_with_score_by_vector(611            embedding=embedding, k=k, filter=filter612        )613        return docs614 615    @property616    def distance_strategy(self) -> Any:617        if self._distance_strategy == DistanceStrategy.EUCLIDEAN:618            return self.EmbeddingStore.embedding.l2_distance619        elif self._distance_strategy == DistanceStrategy.COSINE:620            return self.EmbeddingStore.embedding.cosine_distance621        elif self._distance_strategy == DistanceStrategy.MAX_INNER_PRODUCT:622            return self.EmbeddingStore.embedding.max_inner_product623        else:624            raise ValueError(625                f"Got unexpected value for distance: {self._distance_strategy}. "626                f"Should be one of {', '.join([ds.value for ds in DistanceStrategy])}."627            )628 629    def similarity_search_with_score_by_vector(630        self,631        embedding: List[float],632        k: int = 4,633        filter: Optional[dict] = None,634    ) -> List[Tuple[Document, float]]:635        results = self._query_collection(embedding=embedding, k=k, filter=filter)636 637        return self._results_to_docs_and_scores(results)638 639    def _results_to_docs_and_scores(self, results: Any) -> List[Tuple[Document, float]]:640        """Return docs and scores from results."""641        docs = [642            (643                Document(644                    page_content=result.EmbeddingStore.document,645                    metadata=result.EmbeddingStore.cmetadata,646                ),647                result.distance if self.embedding_function is not None else None,648            )649            for result in results650        ]651        return docs652 653    def _handle_field_filter(654        self,655        field: str,656        value: Any,657    ) -> SQLColumnExpression:658        """Create a filter for a specific field.659 660        Args:661            field: name of field662            value: value to filter663                If provided as is then this will be an equality filter664                If provided as a dictionary then this will be a filter, the key665                will be the operator and the value will be the value to filter by666 667        Returns:668            sqlalchemy expression669        """670        if not isinstance(field, str):671            raise ValueError(672                f"field should be a string but got: {type(field)} with value: {field}"673            )674 675        if field.startswith("$"):676            raise ValueError(677                f"Invalid filter condition. Expected a field but got an operator: "678                f"{field}"679            )680 681        # Allow [a-zA-Z0-9_], disallow $ for now until we support escape characters682        if not field.isidentifier():683            raise ValueError(684                f"Invalid field name: {field}. Expected a valid identifier."685            )686 687        if isinstance(value, dict):688            # This is a filter specification689            if len(value) != 1:690                raise ValueError(691                    "Invalid filter condition. Expected a value which "692                    "is a dictionary with a single key that corresponds to an operator "693                    f"but got a dictionary with {len(value)} keys. The first few "694                    f"keys are: {list(value.keys())[:3]}"695                )696            operator, filter_value = list(value.items())[0]697            # Verify that that operator is an operator698            if operator not in SUPPORTED_OPERATORS:699                raise ValueError(700                    f"Invalid operator: {operator}. "701                    f"Expected one of {SUPPORTED_OPERATORS}"702                )703        else:  # Then we assume an equality operator704            operator = "$eq"705            filter_value = value706 707        if operator in COMPARISONS_TO_NATIVE:708            # Then we implement an equality filter709            # native is trusted input710            native = COMPARISONS_TO_NATIVE[operator]711            return func.jsonb_path_match(712                self.EmbeddingStore.cmetadata,713                f"$.{field} {native} $value",714                json.dumps({"value": filter_value}),715            )716        elif operator == "$between":717            # Use AND with two comparisons718            low, high = filter_value719 720            lower_bound = func.jsonb_path_match(721                self.EmbeddingStore.cmetadata,722                f"$.{field} >= $value",723                json.dumps({"value": low}),724            )725            upper_bound = func.jsonb_path_match(726                self.EmbeddingStore.cmetadata,727                f"$.{field} <= $value",728                json.dumps({"value": high}),729            )730            return sqlalchemy.and_(lower_bound, upper_bound)731        elif operator in {"$in", "$nin", "$like", "$ilike"}:732            # We'll do force coercion to text733            if operator in {"$in", "$nin"}:734                for val in filter_value:735                    if not isinstance(val, (str, int, float)):736                        raise NotImplementedError(737                            f"Unsupported type: {type(val)} for value: {val}"738                        )739 740            queried_field = self.EmbeddingStore.cmetadata[field].astext741 742            if operator in {"$in"}:743                return queried_field.in_([str(val) for val in filter_value])744            elif operator in {"$nin"}:745                return queried_field.not_in([str(val) for val in filter_value])746            elif operator in {"$like"}:747                return queried_field.like(filter_value)748            elif operator in {"$ilike"}:749                return queried_field.ilike(filter_value)750            else:751                raise NotImplementedError()752        else:753            raise NotImplementedError()754 755    def _create_filter_clause_deprecated(756        self, key: str, value: dict[str, Any]757    ) -> SQLColumnExpression:758        """Deprecated functionality.759 760        This is for backwards compatibility with the JSON based schema for metadata.761        It uses incorrect operator syntax (operators are not prefixed with $).762 763        This implementation is not efficient, and has bugs associated with764        the way that it handles numeric filter clauses.765        """766        IN, NIN, BETWEEN, GT, LT, NE = "in", "nin", "between", "gt", "lt", "ne"767        EQ, LIKE, CONTAINS, OR, AND = "eq", "like", "contains", "or", "and"768 769        value_case_insensitive = {k.lower(): v for k, v in value.items()}770        if IN in map(str.lower, value):771            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext.in_(772                value_case_insensitive[IN]773            )774        elif NIN in map(str.lower, value):775            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext.not_in(776                value_case_insensitive[NIN]777            )778        elif BETWEEN in map(str.lower, value):779            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext.between(780                str(value_case_insensitive[BETWEEN][0]),781                str(value_case_insensitive[BETWEEN][1]),782            )783        elif GT in map(str.lower, value):784            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext > str(785                value_case_insensitive[GT]786            )787        elif LT in map(str.lower, value):788            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext < str(789                value_case_insensitive[LT]790            )791        elif NE in map(str.lower, value):792            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext != str(793                value_case_insensitive[NE]794            )795        elif EQ in map(str.lower, value):796            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext == str(797                value_case_insensitive[EQ]798            )799        elif LIKE in map(str.lower, value):800            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext.like(801                value_case_insensitive[LIKE]802            )803        elif CONTAINS in map(str.lower, value):804            filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext.contains(805                value_case_insensitive[CONTAINS]806            )807        elif OR in map(str.lower, value):808            or_clauses = [809                self._create_filter_clause_deprecated(key, sub_value)810                for sub_value in value_case_insensitive[OR]811            ]812            filter_by_metadata = sqlalchemy.or_(*or_clauses)813        elif AND in map(str.lower, value):814            and_clauses = [815                self._create_filter_clause_deprecated(key, sub_value)816                for sub_value in value_case_insensitive[AND]817            ]818            filter_by_metadata = sqlalchemy.and_(*and_clauses)819 820        else:821            filter_by_metadata = None822 823        return filter_by_metadata824 825    def _create_filter_clause_json_deprecated(826        self, filter: Mapping[str, Union[str, dict[str, Any]]]827    ) -> List[SQLColumnExpression]:828        """Convert filters from IR to SQL clauses.829 830        **DEPRECATED** This functionality will be deprecated in the future.831 832        It implements translation of filters for a schema that uses JSON833        for metadata rather than the JSONB field which is more efficient834        for querying.835        """836        filter_clauses = []837        for key, value in filter.items():838            if isinstance(value, dict):839                filter_by_metadata = self._create_filter_clause_deprecated(key, value)840 841                if filter_by_metadata is not None:842                    filter_clauses.append(filter_by_metadata)843            else:844                filter_by_metadata = self.EmbeddingStore.cmetadata[key].astext == str(845                    value846                )847                filter_clauses.append(filter_by_metadata)848        return filter_clauses849 850    def _create_filter_clause(self, filters: Any) -> Any:851        """Convert LangChain IR filter representation to matching SQLAlchemy clauses.852 853        At the top level, we still don't know if we're working with a field854        or an operator for the keys. After we've determined that we can855        call the appropriate logic to handle filter creation.856 857        Args:858            filters: Dictionary of filters to apply to the query.859 860        Returns:861            SQLAlchemy clause to apply to the query.862        """863        if isinstance(filters, dict):864            if len(filters) == 1:865                # The only operators allowed at the top level are $AND and $OR866                # First check if an operator or a field867                key, value = list(filters.items())[0]868                if key.startswith("$"):869                    # Then it's an operator870                    if key.lower() not in ["$and", "$or"]:871                        raise ValueError(872                            f"Invalid filter condition. Expected $and or $or "873                            f"but got: {key}"874                        )875                else:876                    # Then it's a field877                    return self._handle_field_filter(key, filters[key])878 879                # Here we handle the $and and $or operators880                if not isinstance(value, list):881                    raise ValueError(882                        f"Expected a list, but got {type(value)} for value: {value}"883                    )884                if key.lower() == "$and":885                    and_ = [self._create_filter_clause(el) for el in value]886                    if len(and_) > 1:887                        return sqlalchemy.and_(*and_)888                    elif len(and_) == 1:889                        return and_[0]890                    else:891                        raise ValueError(892                            "Invalid filter condition. Expected a dictionary "893                            "but got an empty dictionary"894                        )895                elif key.lower() == "$or":896                    or_ = [self._create_filter_clause(el) for el in value]897                    if len(or_) > 1:898                        return sqlalchemy.or_(*or_)899                    elif len(or_) == 1:900                        return or_[0]901                    else:902                        raise ValueError(903                            "Invalid filter condition. Expected a dictionary "904                            "but got an empty dictionary"905                        )906                else:907                    raise ValueError(908                        f"Invalid filter condition. Expected $and or $or but got: {key}"909                    )910            elif len(filters) > 1:911                # Then all keys have to be fields (they cannot be operators)912                for key in filters.keys():913                    if key.startswith("$"):914                        raise ValueError(915                            f"Invalid filter condition. Expected a field but got: {key}"916                        )917                # These should all be fields and combined using an $and operator918                and_ = [self._handle_field_filter(k, v) for k, v in filters.items()]919                if len(and_) > 1:920                    return sqlalchemy.and_(*and_)921                elif len(and_) == 1:922                    return and_[0]923                else:924                    raise ValueError(925                        "Invalid filter condition. Expected a dictionary "926                        "but got an empty dictionary"927                    )928            else:929                raise ValueError("Got an empty dictionary for filters.")930        else:931            raise ValueError(932                f"Invalid type: Expected a dictionary but got type: {type(filters)}"933            )934 935    def _query_collection(936        self,937        embedding: List[float],938        k: int = 4,939        filter: Optional[Dict[str, str]] = None,940    ) -> List[Any]:941        """Query the collection."""942        with Session(self._bind) as session:943            collection = self.get_collection(session)944            if not collection:945                raise ValueError("Collection not found")946 947            filter_by = [self.EmbeddingStore.collection_id == collection.uuid]948            if filter:949                if self.use_jsonb:950                    filter_clauses = self._create_filter_clause(filter)951                    if filter_clauses is not None:952                        filter_by.append(filter_clauses)953                else:954                    # Old way of doing things955                    filter_clauses = self._create_filter_clause_json_deprecated(filter)956                    filter_by.extend(filter_clauses)957 958            _type = self.EmbeddingStore959 960            results: List[Any] = (961                session.query(962                    self.EmbeddingStore,963                    self.distance_strategy(embedding).label("distance"),964                )965                .filter(*filter_by)966                .order_by(sqlalchemy.asc("distance"))967                .join(968                    self.CollectionStore,969                    self.EmbeddingStore.collection_id == self.CollectionStore.uuid,970                )971                .limit(k)972                .all()973            )974 975        return results976 977    def similarity_search_by_vector(978        self,979        embedding: List[float],980        k: int = 4,981        filter: Optional[dict] = None,982        **kwargs: Any,983    ) -> List[Document]:984        """Return docs most similar to embedding vector.985 986        Args:987            embedding: Embedding to look up documents similar to.988            k: Number of Documents to return. Defaults to 4.989            filter (Optional[Dict[str, str]]): Filter by metadata. Defaults to None.990 991        Returns:992            List of Documents most similar to the query vector.993        """994        docs_and_scores = self.similarity_search_with_score_by_vector(995            embedding=embedding, k=k, filter=filter996        )997        return _results_to_docs(docs_and_scores)998 999    @classmethod1000    def from_texts(1001        cls: Type[PGVector],1002        texts: List[str],1003        embedding: Embeddings,1004        metadatas: Optional[List[dict]] = None,1005        collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME,1006        distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY,1007        ids: Optional[List[str]] = None,1008        pre_delete_collection: bool = False,1009        *,1010        use_jsonb: bool = False,1011        **kwargs: Any,1012    ) -> PGVector:1013        """1014        Return VectorStore initialized from texts and embeddings.1015        Postgres connection string is required1016        "Either pass it as a parameter1017        or set the PGVECTOR_CONNECTION_STRING environment variable.1018        """1019        embeddings = embedding.embed_documents(list(texts))1020 1021        return cls._from(1022            texts,1023            embeddings,1024            embedding,1025            metadatas=metadatas,1026            ids=ids,1027            collection_name=collection_name,1028            distance_strategy=distance_strategy,1029            pre_delete_collection=pre_delete_collection,1030            use_jsonb=use_jsonb,1031            **kwargs,1032        )1033 1034    @classmethod1035    def from_embeddings(1036        cls,1037        text_embeddings: List[Tuple[str, List[float]]],1038        embedding: Embeddings,1039        metadatas: Optional[List[dict]] = None,1040        collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME,1041        distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY,1042        ids: Optional[List[str]] = None,1043        pre_delete_collection: bool = False,1044        **kwargs: Any,1045    ) -> PGVector:1046        """Construct PGVector wrapper from raw documents and pre-1047        generated embeddings.1048 1049        Return VectorStore initialized from documents and embeddings.1050        Postgres connection string is required1051        "Either pass it as a parameter1052        or set the PGVECTOR_CONNECTION_STRING environment variable.1053 1054        Example:1055            .. code-block:: python1056 1057                from langchain_community.vectorstores import PGVector1058                from langchain_community.embeddings import OpenAIEmbeddings1059                embeddings = OpenAIEmbeddings()1060                text_embeddings = embeddings.embed_documents(texts)1061                text_embedding_pairs = list(zip(texts, text_embeddings))1062                faiss = PGVector.from_embeddings(text_embedding_pairs, embeddings)1063        """1064        texts = [t[0] for t in text_embeddings]1065        embeddings = [t[1] for t in text_embeddings]1066 1067        return cls._from(1068            texts,1069            embeddings,1070            embedding,1071            metadatas=metadatas,1072            ids=ids,1073            collection_name=collection_name,1074            distance_strategy=distance_strategy,1075            pre_delete_collection=pre_delete_collection,1076            **kwargs,1077        )1078 1079    @classmethod1080    def from_existing_index(1081        cls: Type[PGVector],1082        embedding: Embeddings,1083        collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME,1084        distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY,1085        pre_delete_collection: bool = False,1086        **kwargs: Any,1087    ) -> PGVector:1088        """1089        Get instance of an existing PGVector store.This method will1090        return the instance of the store without inserting any new1091        embeddings1092        """1093 1094        connection_string = cls.get_connection_string(kwargs)1095 1096        store = cls(1097            connection_string=connection_string,1098            collection_name=collection_name,1099            embedding_function=embedding,1100            distance_strategy=distance_strategy,1101            pre_delete_collection=pre_delete_collection,1102        )1103 1104        return store1105 1106    @classmethod1107    def get_connection_string(cls, kwargs: Dict[str, Any]) -> str:1108        connection_string: str = get_from_dict_or_env(1109            data=kwargs,1110            key="connection_string",1111            env_key="PGVECTOR_CONNECTION_STRING",1112        )1113 1114        if not connection_string:1115            raise ValueError(1116                "Postgres connection string is required"1117                "Either pass it as a parameter"1118                "or set the PGVECTOR_CONNECTION_STRING environment variable."1119            )1120 1121        return connection_string1122 1123    @classmethod1124    def from_documents(1125        cls: Type[PGVector],1126        documents: List[Document],1127        embedding: Embeddings,1128        collection_name: str = _LANGCHAIN_DEFAULT_COLLECTION_NAME,1129        distance_strategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY,1130        ids: Optional[List[str]] = None,1131        pre_delete_collection: bool = False,1132        *,1133        use_jsonb: bool = False,1134        **kwargs: Any,1135    ) -> PGVector:1136        """1137        Return VectorStore initialized from documents and embeddings.1138        Postgres connection string is required1139        "Either pass it as a parameter1140        or set the PGVECTOR_CONNECTION_STRING environment variable.1141        """1142 1143        texts = [d.page_content for d in documents]1144        metadatas = [d.metadata for d in documents]1145        connection_string = cls.get_connection_string(kwargs)1146 1147        kwargs["connection_string"] = connection_string1148 1149        return cls.from_texts(1150            texts=texts,1151            pre_delete_collection=pre_delete_collection,1152            embedding=embedding,1153            distance_strategy=distance_strategy,1154            metadatas=metadatas,1155            ids=ids,1156            collection_name=collection_name,1157            use_jsonb=use_jsonb,1158            **kwargs,1159        )1160 1161    @classmethod1162    def connection_string_from_db_params(1163        cls,1164        driver: str,1165        host: str,1166        port: int,1167        database: str,1168        user: str,1169        password: str,1170    ) -> str:1171        """Return connection string from database parameters."""1172        return f"postgresql+{driver}://{user}:{password}@{host}:{port}/{database}"1173 1174    def _select_relevance_score_fn(self) -> Callable[[float], float]:1175        """1176        The 'correct' relevance function1177        may differ depending on a few things, including:1178        - the distance / similarity metric used by the VectorStore1179        - the scale of your embeddings (OpenAI's are unit normed. Many others are not!)1180        - embedding dimensionality1181        - etc.1182        """1183        if self.override_relevance_score_fn is not None:1184            return self.override_relevance_score_fn1185 1186        # Default strategy is to rely on distance strategy provided1187        # in vectorstore constructor1188        if self._distance_strategy == DistanceStrategy.COSINE:1189            return self._cosine_relevance_score_fn1190        elif self._distance_strategy == DistanceStrategy.EUCLIDEAN:1191            return self._euclidean_relevance_score_fn1192        elif self._distance_strategy == DistanceStrategy.MAX_INNER_PRODUCT:1193            return self._max_inner_product_relevance_score_fn1194        else:1195            raise ValueError(1196                "No supported normalization function"1197                f" for distance_strategy of {self._distance_strategy}."1198                "Consider providing relevance_score_fn to PGVector constructor."1199            )1200 

Showing the first 1,200 of 1388 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai