Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
aperturedb.py555 linesDownload Raw Back to vectorstores
1# System imports2from __future__ import annotations3 4import logging5import time6import uuid7from typing import Any, Dict, List, Optional, Sequence, Tuple, Type8 9# Third-party imports10import numpy as np11 12# Local imports13from langchain_core.documents import Document14from langchain_core.embeddings import Embeddings15from langchain_core.indexing.base import UpsertResponse16from langchain_core.vectorstores import VectorStore17from typing_extensions import override18 19# Configure some defaults20ENGINE = "HNSW"21METRIC = "CS"22DESCRIPTOR_SET = "langchain"23BATCHSIZE = 100024PROPERTY_PREFIX = "lc_"  # Prefix for properties that are in the client metadata25TEXT_PROPERTY = "text"  # Property name for the text26UNIQUEID_PROPERTY = "uniqueid"  # Property name for the unique id27 28 29class ApertureDB(VectorStore):30    @override31    def __init__(32        self,33        embeddings: Embeddings,34        descriptor_set: str = DESCRIPTOR_SET,35        dimensions: Optional[int] = None,36        engine: Optional[str] = None,37        metric: Optional[str] = None,38        log_level: int = logging.WARN,39        properties: Optional[Dict] = None,40        **kwargs: Any,41    ) -> None:42        """Create a vectorstore backed by ApertureDB43 44        A single ApertureDB instance can support many vectorstores,45        distinguished by 'descriptor_set' name.  The descriptor set is created46        if it does not exist.  Different descriptor sets can use different47        engines and metrics, be supplied by different embedding models, and have48        different dimensions.49 50        See ApertureDB documentation on `AddDescriptorSet`51        https://docs.aperturedata.io/query_language/Reference/descriptor_commands/desc_set_commands/AddDescriptorSet52        for more information on the engine and metric options.53 54        Args:55            embeddings (Embeddings): Embeddings object56            descriptor_set (str, optional): Descriptor set name. Defaults to57                "langchain".58            dimensions (Optional[int], optional): Number of dimensions of the59                embeddings. Defaults to None.60            engine (str, optional): Engine to use. Defaults to "HNSW" for new61                descriptorsets.62            metric (str, optional): Metric to use. Defaults to "CS" for new63                descriptorsets.64            log_level (int, optional): Logging level. Defaults to logging.WARN.65        """66        # ApertureDB imports67        try:68            from aperturedb.Utils import Utils, create_connector69        except ImportError:70            raise ImportError(71                "ApertureDB is not installed. Please install it using "72                "'pip install aperturedb'"73            )74 75        super().__init__(**kwargs)76        self.logger = logging.getLogger(__name__)77        self.logger.setLevel(log_level)78        self.descriptor_set = descriptor_set79 80        self.embedding_function = embeddings81        self.dimensions = dimensions82        self.engine = engine83        self.metric = metric84        self.properties = properties85        if embeddings is None:86            self.logger.fatal("No embedding function provided.")87            raise ValueError("No embedding function provided.")88 89        try:90            from aperturedb.Utils import Utils, create_connector91        except ImportError:92            self.logger.exception(93                "ApertureDB is not installed. Please install it using "94                "'pip install aperturedb'"95            )96            raise97 98        self.connection = create_connector()99        self.utils = Utils(self.connection)100        try:101            self.utils.status()102        except Exception:103            self.logger.exception("Failed to connect to ApertureDB")104            raise105 106        self._find_or_add_descriptor_set()107 108    def _find_or_add_descriptor_set(self) -> None:109        descriptor_set = self.descriptor_set110        """Checks if the descriptor set exists, if not, creates it"""111        find_ds_query = [112            {113                "FindDescriptorSet": {114                    "with_name": descriptor_set,115                    "engines": True,116                    "metrics": True,117                    "dimensions": True,118                    "results": {"all_properties": True},119                }120            }121        ]122        r, b = self.connection.query(find_ds_query)123        assert self.connection.last_query_ok(), r124        n_entities = (125            len(r[0]["FindDescriptorSet"]["entities"])126            if "entities" in r[0]["FindDescriptorSet"]127            else 0128        )129        assert n_entities <= 1, "Multiple descriptor sets with the same name"130 131        if n_entities == 1:  # Descriptor set exists already132            e = r[0]["FindDescriptorSet"]["entities"][0]133            self.logger.info(f"Descriptor set {descriptor_set} already exists")134 135            engines = e["_engines"]136            assert len(engines) == 1, "Only one engine is supported"137 138            if self.engine is None:139                self.engine = engines[0]140            elif self.engine != engines[0]:141                self.logger.error(f"Engine mismatch: {self.engine} != {engines[0]}")142 143            metrics = e["_metrics"]144            assert len(metrics) == 1, "Only one metric is supported"145            if self.metric is None:146                self.metric = metrics[0]147            elif self.metric != metrics[0]:148                self.logger.error(f"Metric mismatch: {self.metric} != {metrics[0]}")149 150            dimensions = e["_dimensions"]151            if self.dimensions is None:152                self.dimensions = dimensions153            elif self.dimensions != dimensions:154                self.logger.error(155                    f"Dimensions mismatch: {self.dimensions} != {dimensions}"156                )157 158            self.properties = {159                k[len(PROPERTY_PREFIX) :]: v160                for k, v in e.items()161                if k.startswith(PROPERTY_PREFIX)162            }163 164        else:165            self.logger.info(166                f"Descriptor set {descriptor_set} does not exist. Creating it"167            )168            if self.engine is None:169                self.engine = ENGINE170            if self.metric is None:171                self.metric = METRIC172            if self.dimensions is None:173                self.dimensions = len(self.embedding_function.embed_query("test"))174 175            properties = (176                {PROPERTY_PREFIX + k: v for k, v in self.properties.items()}177                if self.properties is not None178                else None179            )180 181            self.utils.add_descriptorset(182                name=descriptor_set,183                dim=self.dimensions,184                engine=self.engine,185                metric=self.metric,186                properties=properties,187            )188 189            # Create indexes190            self.utils.create_entity_index("_Descriptor", "_create_txn")191            self.utils.create_entity_index("_DescriptorSet", "_name")192            self.utils.create_entity_index("_Descriptor", UNIQUEID_PROPERTY)193 194    @override195    def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[bool]:196        """Delete documents from the vectorstore by id.197 198        Args:199            ids: List of ids to delete from the vectorstore.200 201        Returns:202            True if the deletion was successful, False otherwise203        """204        assert ids is not None, "ids must be provided"205        query = [206            {207                "DeleteDescriptor": {208                    "set": self.descriptor_set,209                    "constraints": {UNIQUEID_PROPERTY: ["in", ids]},210                }211            }212        ]213 214        result, _ = self.utils.execute(query)215        return result216 217    @override218    def get_by_ids(self, ids: Sequence[str], /) -> List[Document]:219        """Find documents in the vectorstore by id.220 221        Args:222            ids: List of ids to find in the vectorstore.223 224        Returns:225            documents: List of Document objects found in the vectorstore.226        """227        query = [228            {229                "FindDescriptor": {230                    "set": self.descriptor_set,231                    "constraints": {UNIQUEID_PROPERTY: ["in", ids]},232                    "results": {"all_properties": True},233                }234            }235        ]236 237        results, _ = self.utils.execute(query)238        docs = [239            self._descriptor_to_document(d)240            for d in results[0]["FindDescriptor"].get("entities", [])241        ]242        return docs243 244    @override245    def similarity_search(246        self, query: str, k: int = 4, *args: Any, **kwargs: Any247    ) -> List[Document]:248        """Search for documents similar to the query using the vectorstore249 250        Args:251            query: Query string to search for.252            k: Number of results to return.253 254        Returns:255            List of Document objects ordered by decreasing similarity to the query.256        """257        assert self.embedding_function is not None, "Embedding function is not set"258        embedding = self.embedding_function.embed_query(query)259        return self.similarity_search_by_vector(embedding, k, *args, **kwargs)260 261    @override262    def similarity_search_with_score(263        self, query: str, *args: Any, **kwargs: Any264    ) -> List[Tuple[Document, float]]:265        embedding = self.embedding_function.embed_query(query)266        return self._similarity_search_with_score_by_vector(embedding, *args, **kwargs)267 268    def _descriptor_to_document(self, d: dict) -> Document:269        metadata = {}270        for k, v in d.items():271            if k.startswith(PROPERTY_PREFIX):272                metadata[k[len(PROPERTY_PREFIX) :]] = v273        text = d[TEXT_PROPERTY]274        uniqueid = d[UNIQUEID_PROPERTY]275        doc = Document(page_content=text, metadata=metadata, id=uniqueid)276        return doc277 278    def _similarity_search_with_score_by_vector(279        self, embedding: List[float], k: int = 4, vectors: bool = False280    ) -> List[Tuple[Document, float]]:281        from aperturedb.Descriptors import Descriptors282 283        descriptors = Descriptors(self.connection)284        start_time = time.time()285        descriptors.find_similar(286            set=self.descriptor_set, vector=embedding, k_neighbors=k, distances=True287        )288        self.logger.info(289            f"ApertureDB similarity search took {time.time() - start_time} seconds"290        )291        return [(self._descriptor_to_document(d), d["_distance"]) for d in descriptors]292 293    @override294    def similarity_search_by_vector(295        self, embedding: List[float], k: int = 4, **kwargs: Any296    ) -> List[Document]:297        """Returns the k most similar documents to the given embedding vector298 299        Args:300            embedding: The embedding vector to search for301            k: The number of similar documents to return302 303        Returns:304            List of Document objects ordered by decreasing similarity to the query.305        """306        from aperturedb.Descriptors import Descriptors307 308        descriptors = Descriptors(self.connection)309        start_time = time.time()310        descriptors.find_similar(311            set=self.descriptor_set, vector=embedding, k_neighbors=k312        )313        self.logger.info(314            f"ApertureDB similarity search took {time.time() - start_time} seconds"315        )316        return [self._descriptor_to_document(d) for d in descriptors]317 318    @override319    def max_marginal_relevance_search(320        self,321        query: str,322        k: int = 4,323        fetch_k: int = 20,324        lambda_mult: float = 0.5,325        **kwargs: Any,326    ) -> List[Document]:327        """Returns similar documents to the query that also have diversity328 329        This algorithm balances relevance and diversity in the search results.330 331        Args:332            query: Query string to search for.333            k: Number of results to return.334            fetch_k: Number of results to fetch.335            lambda_mult: Lambda multiplier for MMR.336 337        Returns:338            List of Document objects ordered by decreasing similarity/diversty.339        """340        self.logger.info(f"Max Marginal Relevance search for query: {query}")341        embedding = self.embedding_function.embed_query(query)342        return self.max_marginal_relevance_search_by_vector(343            embedding, k, fetch_k, lambda_mult, **kwargs344        )345 346    @override347    def max_marginal_relevance_search_by_vector(348        self,349        embedding: List[float],350        k: int = 4,351        fetch_k: int = 20,352        lambda_mult: float = 0.5,353        **kwargs: Any,354    ) -> List[Document]:355        """Returns similar documents to the vector that also have diversity356 357        This algorithm balances relevance and diversity in the search results.358 359        Args:360            embedding: Embedding vector to search for.361            k: Number of results to return.362            fetch_k: Number of results to fetch.363            lambda_mult: Lambda multiplier for MMR.364 365        Returns:366            List of Document objects ordered by decreasing similarity/diversty.367        """368        from aperturedb.Descriptors import Descriptors369 370        descriptors = Descriptors(self.connection)371        start_time = time.time()372        descriptors.find_similar_mmr(373            set=self.descriptor_set,374            vector=embedding,375            k_neighbors=k,376            fetch_k=fetch_k,377            lambda_mult=lambda_mult,378        )379        self.logger.info(380            f"ApertureDB similarity search mmr took {time.time() - start_time} seconds"381        )382        return [self._descriptor_to_document(d) for d in descriptors]383 384    @classmethod385    @override386    def from_texts(387        cls: Type[ApertureDB],388        texts: List[str],389        embedding: Embeddings,390        metadatas: Optional[List[dict]] = None,391        **kwargs: Any,392    ) -> ApertureDB:393        """Creates a new vectorstore from a list of texts394 395        Args:396            texts: List of text strings397            embedding: Embeddings object as for constructing the vectorstore398            metadatas: Optional list of metadatas associated with the texts.399            kwargs: Additional arguments to pass to the constructor400        """401        store = cls(embeddings=embedding, **kwargs)402        store.add_texts(texts, metadatas)403        return store404 405    @classmethod406    @override407    def from_documents(408        cls: Type[ApertureDB],409        documents: List[Document],410        embedding: Embeddings,411        **kwargs: Any,412    ) -> ApertureDB:413        """Creates a new vectorstore from a list of documents414 415        Args:416            documents: List of Document objects417            embedding: Embeddings object as for constructing the vectorstore418            metadatas: Optional list of metadatas associated with the texts.419            kwargs: Additional arguments to pass to the constructor420        """421        store = cls(embeddings=embedding, **kwargs)422        store.add_documents(documents)423        return store424 425    @classmethod426    def delete_vectorstore(class_, descriptor_set: str) -> None:427        """Deletes a vectorstore and all its data from the database428 429        Args:430            descriptor_set: The name of the descriptor set to delete431        """432        from aperturedb.Utils import Utils, create_connector433 434        db = create_connector()435        utils = Utils(db)436        utils.remove_descriptorset(descriptor_set)437 438    @classmethod439    def list_vectorstores(class_) -> None:440        """Returns a list of all vectorstores in the database441 442        Returns:443            List of descriptor sets with properties444        """445        from aperturedb.Utils import create_connector446 447        db = create_connector()448        query = [449            {450                "FindDescriptorSet": {451                    # Return all properties452                    "results": {"all_properties": True},453                    "engines": True,454                    "metrics": True,455                    "dimensions": True,456                }457            }458        ]459        response, _ = db.query(query)460        assert db.last_query_ok(), response461        return response[0]["FindDescriptorSet"]["entities"]462 463    def add_documents(self, documents: List[Document], **kwargs: Any) -> List[str]:464        """Add or update documents in the vectorstore.465 466        Args:467            documents: Documents to add to the vectorstore.468            kwargs: Additional keyword arguments.469                if kwargs contains ids and documents contain ids,470                the ids in the kwargs will receive precedence.471 472        Returns:473            List of IDs of the added texts.474 475        Raises:476            ValueError: If the number of ids does not match the number of documents.477        """478 479        if "ids" in kwargs:480            ids = kwargs.pop("ids")481            if ids and len(ids) != len(documents):482                raise ValueError(483                    "The number of ids must match the number of documents. "484                    "Got {len(ids)} ids and {len(documents)} documents."485                )486 487            documents_ = []488 489            for id_, document in zip(ids, documents):490                doc_with_id = Document(491                    page_content=document.page_content,492                    metadata=document.metadata,493                    id=id_,494                )495                documents_.append(doc_with_id)496        else:497            documents_ = documents498 499        # If upsert has been implemented, we can use it to add documents500        return self.upsert(documents_, **kwargs)["succeeded"]501 502    def upsert(self, items: Sequence[Document], /, **kwargs: Any) -> UpsertResponse:503        """Insert or update items504 505        Updating documents is dependent on the documents' `id` attribute.506 507        Args:508            items: List of Document objects to upsert509 510        Returns:511            UpsertResponse object with succeeded and failed512        """513        # For now, simply delete and add514        # We could do something more efficient to update metadata,515        # but we don't support changing the embedding of a descriptor.516 517        from aperturedb.ParallelLoader import ParallelLoader518 519        ids_to_delete: List[str] = [520            item.id for item in items if hasattr(item, "id") and item.id is not None521        ]522        if ids_to_delete:523            self.delete(ids_to_delete)524 525        texts = [doc.page_content for doc in items]526        metadatas = [527            doc.metadata if getattr(doc, "metadata", None) is not None else {}528            for doc in items529        ]530        embeddings = self.embedding_function.embed_documents(texts)531        ids: List[str] = [532            doc.id if hasattr(doc, "id") and doc.id is not None else str(uuid.uuid4())533            for doc in items534        ]535 536        data = []537        for text, embedding, metadata, unique_id in zip(538            texts, embeddings, metadatas, ids539        ):540            properties = {PROPERTY_PREFIX + k: v for k, v in metadata.items()}541            properties[TEXT_PROPERTY] = text542            properties[UNIQUEID_PROPERTY] = unique_id543            command = {544                "AddDescriptor": {545                    "set": self.descriptor_set,546                    "properties": properties,547                }548            }549            query = [command]550            blobs = [np.array(embedding, dtype=np.float32).tobytes()]551            data.append((query, blobs))552        loader = ParallelLoader(self.connection)553        loader.ingest(data, batchsize=BATCHSIZE)554        return UpsertResponse(succeeded=ids, failed=[])555 
codekingpro/portable-devtools · Team Ai