From 5c026e839f5b55a1b1670f32f974a96705ebe69f Mon Sep 17 00:00:00 2001
From: Tushar Sonawane <tusharlsonawane@gmail.com>
Date: Fri, 21 Feb 2025 14:53:50 +0530
Subject: [PATCH] feat(vectorstore): adds firestore vector store support
 (#1600)

---
 .changeset/lemon-pumas-end.md                 |   5 +
 examples/firestore/.env.template              |   4 +
 examples/firestore/README.md                  |  35 +++
 examples/firestore/delete.ts                  |  32 ++
 examples/firestore/load.ts                    |  44 +++
 examples/firestore/query.ts                   |  58 ++++
 examples/package.json                         |   1 +
 .../providers/storage/firestore/CHANGELOG.md  |   0
 .../providers/storage/firestore/package.json  |  48 +++
 .../firestore/src/FirestoreVectorStore.ts     | 273 ++++++++++++++++++
 .../providers/storage/firestore/src/index.ts  |   1 +
 .../providers/storage/firestore/tsconfig.json |  19 ++
 pnpm-lock.yaml                                | 170 ++++++++++-
 tsconfig.json                                 |   3 +
 14 files changed, 691 insertions(+), 2 deletions(-)
 create mode 100644 .changeset/lemon-pumas-end.md
 create mode 100644 examples/firestore/.env.template
 create mode 100644 examples/firestore/README.md
 create mode 100644 examples/firestore/delete.ts
 create mode 100644 examples/firestore/load.ts
 create mode 100644 examples/firestore/query.ts
 create mode 100644 packages/providers/storage/firestore/CHANGELOG.md
 create mode 100644 packages/providers/storage/firestore/package.json
 create mode 100644 packages/providers/storage/firestore/src/FirestoreVectorStore.ts
 create mode 100644 packages/providers/storage/firestore/src/index.ts
 create mode 100644 packages/providers/storage/firestore/tsconfig.json

diff --git a/.changeset/lemon-pumas-end.md b/.changeset/lemon-pumas-end.md
new file mode 100644
index 000000000..b4318ae9f
--- /dev/null
+++ b/.changeset/lemon-pumas-end.md
@@ -0,0 +1,5 @@
+---
+"@llamaindex/firestore": major
+---
+
+Firestore vector store support for LlamaIndex
diff --git a/examples/firestore/.env.template b/examples/firestore/.env.template
new file mode 100644
index 000000000..03bdb03de
--- /dev/null
+++ b/examples/firestore/.env.template
@@ -0,0 +1,4 @@
+GCP_PROJECT_ID=
+GCP_CREDENTIALS=
+FIRESTORE_DB=
+OPENAI_API_KEY=
\ No newline at end of file
diff --git a/examples/firestore/README.md b/examples/firestore/README.md
new file mode 100644
index 000000000..22764c2c3
--- /dev/null
+++ b/examples/firestore/README.md
@@ -0,0 +1,35 @@
+# Firestore Vector Store
+
+Here are some sample scripts which work with loading and querying data from a Firestore Vector Store.
+
+## Prerequisites
+
+- A Firestore Database
+  - Hosted https://console.firebase.google.com/
+- An OpenAI API Key
+
+## Setup
+
+1. Set your env variables:
+
+- `FIRESTORE_DB`: Name of your Firestore database
+- `GCP_PROJECT_ID`: Your GCP project ID
+- `GCP_CREDENTIALS`: Your GCP credentials JSON
+- `OPENAI_API_KEY`: Your OpenAI key
+
+2. `cd` Into the `examples` directory
+3. run `npm i`
+
+## Load the data
+
+This sample loads the same dataset of movie reviews as sample dataset
+
+run `npx tsx firestore/load.ts`
+
+## Use RAG to Query the data
+
+run `npx tsx firestore/query.ts`
+
+## Delete the data
+
+run `npx tsx firestore/delete.ts`
diff --git a/examples/firestore/delete.ts b/examples/firestore/delete.ts
new file mode 100644
index 000000000..6a6e1b07e
--- /dev/null
+++ b/examples/firestore/delete.ts
@@ -0,0 +1,32 @@
+import { CollectionReference } from "@google-cloud/firestore";
+import "dotenv/config";
+
+import { FirestoreVectorStore } from "@llamaindex/firestore";
+import { OpenAIEmbedding, Settings } from "llamaindex";
+
+const indexName = "MovieReviews";
+
+Settings.embedModel = new OpenAIEmbedding();
+
+async function main() {
+  try {
+    const vectorStore = new FirestoreVectorStore({
+      clientOptions: {
+        credentials: JSON.parse(process.env.GCP_CREDENTIALS!),
+        projectId: process.env.GCP_PROJECT_ID!,
+        databaseId: process.env.FIRESTORE_DB!,
+        ignoreUndefinedProperties: true,
+      },
+      collectionName: indexName,
+      customCollectionReference: (rootCollection: CollectionReference) => {
+        return rootCollection.doc("accountId-123").collection("vectors");
+      },
+    });
+
+    vectorStore.delete("movie_reviews.csv");
+  } catch (e) {
+    console.error(e);
+  }
+}
+
+void main();
diff --git a/examples/firestore/load.ts b/examples/firestore/load.ts
new file mode 100644
index 000000000..9a04fc5c9
--- /dev/null
+++ b/examples/firestore/load.ts
@@ -0,0 +1,44 @@
+import { CollectionReference } from "@google-cloud/firestore";
+import { CSVReader } from "@llamaindex/readers/csv";
+import "dotenv/config";
+
+import {
+  OpenAIEmbedding,
+  Settings,
+  storageContextFromDefaults,
+  VectorStoreIndex,
+} from "llamaindex";
+
+import { FirestoreVectorStore } from "@llamaindex/firestore";
+
+const indexName = "MovieReviews";
+
+Settings.embedModel = new OpenAIEmbedding();
+
+async function main() {
+  try {
+    const reader = new CSVReader(false);
+    const docs = await reader.loadData("./data/movie_reviews.csv");
+
+    const vectorStore = new FirestoreVectorStore({
+      clientOptions: {
+        credentials: JSON.parse(process.env.GCP_CREDENTIALS!),
+        projectId: process.env.GCP_PROJECT_ID!,
+        databaseId: process.env.FIRESTORE_DB!,
+        ignoreUndefinedProperties: true,
+      },
+      collectionName: indexName,
+      customCollectionReference: (rootCollection: CollectionReference) => {
+        return rootCollection.doc("accountId-123").collection("vectors");
+      },
+    });
+
+    const storageContext = await storageContextFromDefaults({ vectorStore });
+
+    await VectorStoreIndex.fromDocuments(docs, { storageContext });
+  } catch (e) {
+    console.error(e);
+  }
+}
+
+void main();
diff --git a/examples/firestore/query.ts b/examples/firestore/query.ts
new file mode 100644
index 000000000..30a6d53ce
--- /dev/null
+++ b/examples/firestore/query.ts
@@ -0,0 +1,58 @@
+import "dotenv/config";
+
+import { OpenAIEmbedding, Settings, VectorStoreIndex } from "llamaindex";
+
+import { CollectionReference } from "@google-cloud/firestore";
+import { FirestoreVectorStore } from "@llamaindex/firestore";
+
+const indexName = "MovieReviews";
+
+Settings.embedModel = new OpenAIEmbedding();
+
+async function main() {
+  try {
+    const vectorStore = new FirestoreVectorStore({
+      clientOptions: {
+        credentials: JSON.parse(process.env.GCP_CREDENTIALS!),
+        projectId: process.env.GCP_PROJECT_ID!,
+        databaseId: process.env.FIRESTORE_DB!,
+        ignoreUndefinedProperties: true,
+      },
+      collectionName: indexName,
+      customCollectionReference: (rootCollection: CollectionReference) => {
+        return rootCollection.doc("accountId-123").collection("vectors");
+      },
+    });
+    const index = await VectorStoreIndex.fromVectorStore(vectorStore);
+    const retriever = index.asRetriever({ similarityTopK: 20 });
+
+    const queryEngine = index.asQueryEngine({ retriever });
+    const query = "Get all movie titles.";
+    const results = await queryEngine.query({ query });
+    console.log(`Query from ${results.sourceNodes?.length} nodes`);
+    console.log(results.response);
+
+    console.log("\n=====\nQuerying the index with filters");
+    const queryEngineWithFilters = index.asQueryEngine({
+      retriever,
+      preFilters: {
+        filters: [
+          {
+            key: "file_name",
+            value: "movie_reviews.csv",
+            operator: "==",
+          },
+        ],
+      },
+    });
+    const resultAfterFilter = await queryEngineWithFilters.query({
+      query: "Get all movie titles.",
+    });
+    console.log(`Query from ${resultAfterFilter.sourceNodes?.length} nodes`);
+    console.log(resultAfterFilter.response);
+  } catch (e) {
+    console.error(e);
+  }
+}
+
+void main();
diff --git a/examples/package.json b/examples/package.json
index 97db00b43..3ddfbcc85 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -21,6 +21,7 @@
     "@llamaindex/core": "^0.5.1",
     "@llamaindex/deepinfra": "^0.0.37",
     "@llamaindex/env": "^0.1.28",
+    "@llamaindex/firestore": "^0.0.1",
     "@llamaindex/google": "^0.0.8",
     "@llamaindex/groq": "^0.0.52",
     "@llamaindex/huggingface": "^0.0.37",
diff --git a/packages/providers/storage/firestore/CHANGELOG.md b/packages/providers/storage/firestore/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/providers/storage/firestore/package.json b/packages/providers/storage/firestore/package.json
new file mode 100644
index 000000000..ea7e8d850
--- /dev/null
+++ b/packages/providers/storage/firestore/package.json
@@ -0,0 +1,48 @@
+{
+  "name": "@llamaindex/firestore",
+  "description": "Firestore Storage for LlamaIndex",
+  "version": "0.0.1",
+  "type": "module",
+  "main": "./dist/index.cjs",
+  "module": "./dist/index.js",
+  "exports": {
+    ".": {
+      "edge-light": {
+        "types": "./dist/index.edge-light.d.ts",
+        "default": "./dist/index.edge-light.js"
+      },
+      "workerd": {
+        "types": "./dist/index.edge-light.d.ts",
+        "default": "./dist/index.edge-light.js"
+      },
+      "require": {
+        "types": "./dist/index.d.cts",
+        "default": "./dist/index.cjs"
+      },
+      "import": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.js"
+      }
+    }
+  },
+  "files": [
+    "dist"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/run-llama/LlamaIndexTS.git",
+    "directory": "packages/providers/storage/firestore"
+  },
+  "scripts": {
+    "build": "bunchee",
+    "dev": "bunchee --watch"
+  },
+  "devDependencies": {
+    "bunchee": "6.3.4"
+  },
+  "dependencies": {
+    "@llamaindex/core": "workspace:*",
+    "@llamaindex/env": "workspace:*",
+    "@google-cloud/firestore": "^7.11.0"
+  }
+}
diff --git a/packages/providers/storage/firestore/src/FirestoreVectorStore.ts b/packages/providers/storage/firestore/src/FirestoreVectorStore.ts
new file mode 100644
index 000000000..31a357616
--- /dev/null
+++ b/packages/providers/storage/firestore/src/FirestoreVectorStore.ts
@@ -0,0 +1,273 @@
+import {
+  CollectionReference,
+  FieldValue,
+  Filter,
+  Firestore,
+  type Settings,
+  type VectorQuery,
+  type WhereFilterOp,
+} from "@google-cloud/firestore";
+import type { BaseNode, Metadata } from "@llamaindex/core/schema";
+import {
+  BaseVectorStore,
+  FilterOperator,
+  metadataDictToNode,
+  nodeToMetadata,
+  type MetadataFilter,
+  type MetadataFilters,
+  type VectorStoreBaseParams,
+  type VectorStoreQuery,
+  type VectorStoreQueryResult,
+} from "@llamaindex/core/vector-store";
+
+enum DistanceMeasure {
+  COSINE = "COSINE",
+  EUCLIDEAN = "EUCLIDEAN",
+  DOT_PRODUCT = "DOT_PRODUCT",
+}
+
+type FirestoreParams = {
+  collectionName: string;
+  client?: Firestore;
+  clientOptions?: {
+    credentials: Settings["credentials"];
+    projectId: string;
+    databaseId: string;
+    ignoreUndefinedProperties: boolean;
+  };
+  batchSize?: number;
+  embeddingKey?: string;
+  textKey?: string;
+  metadataKey?: string;
+  distanceMeasure?: DistanceMeasure;
+  customCollectionReference?: (
+    rootCollection: CollectionReference,
+  ) => CollectionReference;
+} & VectorStoreBaseParams;
+
+const DEFAULT_BATCH_SIZE = 500;
+
+function toFirestoreOperator(operator: FilterOperator): WhereFilterOp {
+  const operatorMap: Record<FilterOperator, WhereFilterOp> = {
+    [FilterOperator.EQ]: "==",
+    [FilterOperator.NE]: "!=",
+    [FilterOperator.GT]: ">",
+    [FilterOperator.GTE]: ">=",
+    [FilterOperator.LT]: "<",
+    [FilterOperator.LTE]: "<=",
+    [FilterOperator.IN]: "in",
+    [FilterOperator.NIN]: "not-in",
+    [FilterOperator.CONTAINS]: "array-contains",
+    [FilterOperator.TEXT_MATCH]: "==",
+    [FilterOperator.ANY]: "array-contains-any",
+    [FilterOperator.ALL]: "array-contains",
+    [FilterOperator.IS_EMPTY]: "==",
+  };
+
+  const firestoreOp = operatorMap[operator];
+  if (!firestoreOp) {
+    throw new Error(`Operator ${operator} not supported in Firestore.`);
+  }
+  return firestoreOp;
+}
+
+function toFirestoreFilter(filters: MetadataFilters): Filter | undefined {
+  if (!filters?.filters?.length) return undefined;
+
+  const firestoreFilters = filters.filters.map((filter: MetadataFilter) => {
+    const path = `${filter.key}`;
+    const operator = toFirestoreOperator(filter.operator as FilterOperator);
+    return Filter.where(path, operator, filter.value);
+  });
+
+  if (firestoreFilters.length === 1) {
+    return firestoreFilters[0];
+  }
+
+  return filters.condition === "or"
+    ? Filter.or(...firestoreFilters)
+    : Filter.and(...firestoreFilters);
+}
+
+export class FirestoreVectorStore extends BaseVectorStore<Firestore> {
+  storesText: boolean = true;
+  isEmbeddingQuery?: boolean = false;
+  flatMetadata: boolean = true;
+
+  private firestoreClient: Firestore;
+  private collectionName: string;
+  private batchSize: number;
+  private embeddingKey: string = "embedding";
+  private metadataKey: string = "metadata";
+  private distanceMeasure: DistanceMeasure = DistanceMeasure.COSINE;
+  private customCollectionReference: (
+    rootCollection: CollectionReference,
+  ) => CollectionReference;
+
+  constructor({
+    collectionName = "vector_store",
+    client,
+    clientOptions,
+    batchSize = DEFAULT_BATCH_SIZE,
+    distanceMeasure = DistanceMeasure.COSINE,
+    customCollectionReference,
+    ...init
+  }: FirestoreParams) {
+    super(init);
+    this.collectionName = collectionName;
+    this.batchSize = batchSize;
+    this.distanceMeasure = distanceMeasure;
+    this.customCollectionReference =
+      customCollectionReference ?? ((rootCollection) => rootCollection);
+
+    if (client) {
+      this.firestoreClient = client;
+    } else {
+      if (!clientOptions) {
+        throw new Error("clientOptions are required");
+      }
+      if (!clientOptions.credentials) {
+        throw new Error("clientOptions.credentials are required");
+      }
+      if (!clientOptions.projectId) {
+        throw new Error("clientOptions.projectId is required");
+      }
+      this.firestoreClient = new Firestore({
+        credentials: clientOptions.credentials,
+        projectId: clientOptions.projectId,
+        databaseId: clientOptions?.databaseId,
+        ignoreUndefinedProperties:
+          clientOptions.ignoreUndefinedProperties ?? false,
+      });
+    }
+  }
+
+  public client() {
+    return this.firestoreClient;
+  }
+
+  /**
+   * Adds nodes to the vector store
+   * @param {BaseNode<Metadata>[]} nodes - Array of nodes to add to the vector store
+   * @returns {Promise<string[]>} Array of node IDs that were added
+   */
+  async add(nodes: BaseNode<Metadata>[]): Promise<string[]> {
+    const batch = this.firestoreClient.batch();
+    const collection = this.customCollectionReference(
+      this.firestoreClient.collection(this.collectionName),
+    );
+
+    const ids: string[] = [];
+
+    for (const node of nodes) {
+      const docRef = collection.doc(node.id_);
+      const metadata = nodeToMetadata(
+        node,
+        !this.storesText,
+        "text",
+        this.flatMetadata,
+      );
+      const entry = {
+        [this.embeddingKey]: FieldValue.vector(node.getEmbedding()),
+        [this.metadataKey]: metadata,
+      };
+
+      batch.set(docRef, entry, { merge: true });
+
+      ids.push(node.id_);
+
+      // Commit batch when it reaches the size limit
+      if (ids.length % this.batchSize === 0) {
+        await batch.commit();
+      }
+    }
+
+    // Commit any remaining documents
+    if (nodes.length % this.batchSize !== 0) {
+      await batch.commit();
+    }
+
+    return ids;
+  }
+
+  /**
+   * Deletes all nodes from the vector store that match the given filename
+   * @param {string} fileName - Name of the file whose nodes should be deleted
+   * @returns {Promise<void>}
+   */
+  async delete(fileName: string): Promise<void> {
+    const collection = this.customCollectionReference(
+      this.firestoreClient.collection(this.collectionName),
+    );
+    const snapshot = await collection
+      .where(`${this.metadataKey}.file_name`, "==", fileName)
+      .get();
+
+    const batch = this.firestoreClient.batch();
+    snapshot.docs.forEach((doc) => {
+      batch.delete(doc.ref);
+    });
+    await batch.commit();
+  }
+
+  /**
+   * Queries the vector store for similar nodes
+   * @param {VectorStoreQuery} query - Query parameters including queryStr or queryEmbedding, filters, and similarityTopK
+   * @param {object} [_options] - Optional parameters for the query
+   * @returns {Promise<VectorStoreQueryResult>} Query results containing matching nodes, their similarities, and IDs
+   * @throws {Error} When neither queryEmbedding nor queryStr is provided
+   */
+  async query(
+    query: VectorStoreQuery,
+    _options?: object,
+  ): Promise<VectorStoreQueryResult> {
+    if (!query.queryEmbedding) {
+      throw new Error("No query embedding provided");
+    }
+
+    // Get documents with filters if any
+    let baseQuery = this.firestoreClient.collection(this.collectionName);
+    baseQuery = this.customCollectionReference(baseQuery);
+    if (query.filters) {
+      const filter = toFirestoreFilter(query.filters);
+      if (filter) {
+        baseQuery = baseQuery.where(filter) as CollectionReference;
+      }
+    }
+
+    // Use Firestore's native vector search
+    const vectorQuery = baseQuery.findNearest({
+      vectorField: this.embeddingKey,
+      queryVector: query.queryEmbedding,
+      limit: query.similarityTopK,
+      distanceMeasure: this.distanceMeasure,
+      distanceResultField: "vector_distance",
+    }) as VectorQuery;
+
+    const snapshot = await vectorQuery.get();
+
+    // Convert results to VectorStoreQueryResult format
+    const topKIds: string[] = [];
+    const topKNodes: BaseNode[] = [];
+    const topKSimilarities: number[] = [];
+
+    snapshot.forEach((doc) => {
+      const distance = doc.get("vector_distance") as number;
+      // Convert distance to similarity score (1 - normalized distance)
+      const similarity =
+        this.distanceMeasure === DistanceMeasure.DOT_PRODUCT
+          ? distance // For dot product, higher is more similar
+          : 1 / (1 + distance); // For EUCLIDEAN and COSINE, lower distance means more similar
+
+      topKIds.push(doc.id);
+      topKNodes.push(metadataDictToNode(doc.get(this.metadataKey)));
+      topKSimilarities.push(similarity);
+    });
+
+    return {
+      nodes: topKNodes,
+      similarities: topKSimilarities,
+      ids: topKIds,
+    };
+  }
+}
diff --git a/packages/providers/storage/firestore/src/index.ts b/packages/providers/storage/firestore/src/index.ts
new file mode 100644
index 000000000..efb0a7a09
--- /dev/null
+++ b/packages/providers/storage/firestore/src/index.ts
@@ -0,0 +1 @@
+export * from "./FirestoreVectorStore";
diff --git a/packages/providers/storage/firestore/tsconfig.json b/packages/providers/storage/firestore/tsconfig.json
new file mode 100644
index 000000000..4607e1860
--- /dev/null
+++ b/packages/providers/storage/firestore/tsconfig.json
@@ -0,0 +1,19 @@
+{
+  "extends": "../../../../tsconfig.json",
+  "compilerOptions": {
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "outDir": "./lib",
+    "tsBuildInfoFile": "./lib/.tsbuildinfo"
+  },
+  "include": ["./src"],
+  "references": [
+    {
+      "path": "../../../core/tsconfig.json"
+    },
+    {
+      "path": "../../../env/tsconfig.json"
+    }
+  ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c76c1225f..e8f40eb6e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -625,6 +625,9 @@ importers:
       '@llamaindex/env':
         specifier: ^0.1.28
         version: link:../packages/env
+      '@llamaindex/firestore':
+        specifier: ^0.0.1
+        version: link:../packages/providers/storage/firestore
       '@llamaindex/google':
         specifier: ^0.0.8
         version: link:../packages/providers/google
@@ -1449,6 +1452,22 @@ importers:
         specifier: 6.3.4
         version: 6.3.4(patch_hash=pavboztthlgni7m5gzw7643oru)(typescript@5.7.3)
 
+  packages/providers/storage/firestore:
+    dependencies:
+      '@google-cloud/firestore':
+        specifier: ^7.11.0
+        version: 7.11.0
+      '@llamaindex/core':
+        specifier: workspace:*
+        version: link:../../../core
+      '@llamaindex/env':
+        specifier: workspace:*
+        version: link:../../../env
+    devDependencies:
+      bunchee:
+        specifier: 6.3.4
+        version: 6.3.4(patch_hash=pavboztthlgni7m5gzw7643oru)(typescript@5.7.3)
+
   packages/providers/storage/milvus:
     dependencies:
       '@grpc/grpc-js':
@@ -3057,6 +3076,10 @@ packages:
   '@gerrit0/mini-shiki@1.27.2':
     resolution: {integrity: sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==}
 
+  '@google-cloud/firestore@7.11.0':
+    resolution: {integrity: sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==}
+    engines: {node: '>=14.0.0'}
+
   '@google-cloud/vertexai@1.9.0':
     resolution: {integrity: sha512-8brlcJwFXI4fPuBtsDNQqCdWZmz8gV9jeEKOU0vc5H2SjehCQpXK/NwuSEr916zbhlBHtg/sU37qQQdgvh5BRA==}
     engines: {node: '>=18.0.0'}
@@ -4961,6 +4984,10 @@ packages:
   '@tokenizer/token@0.3.0':
     resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
 
+  '@tootallnate/once@2.0.0':
+    resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
+    engines: {node: '>= 10'}
+
   '@tootallnate/quickjs-emscripten@0.23.0':
     resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
 
@@ -4998,6 +5025,9 @@ packages:
   '@types/babel__traverse@7.20.6':
     resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
 
+  '@types/caseless@0.12.5':
+    resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
+
   '@types/cookie@0.6.0':
     resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
 
@@ -5103,6 +5133,9 @@ packages:
   '@types/readable-stream@4.0.18':
     resolution: {integrity: sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==}
 
+  '@types/request@2.48.12':
+    resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==}
+
   '@types/resolve@1.20.2':
     resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
 
@@ -6537,6 +6570,9 @@ packages:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
 
+  duplexify@4.1.3:
+    resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
+
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
@@ -7075,6 +7111,10 @@ packages:
     resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==}
     engines: {node: '>= 18'}
 
+  form-data@2.5.2:
+    resolution: {integrity: sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==}
+    engines: {node: '>= 0.12'}
+
   form-data@4.0.0:
     resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
     engines: {node: '>= 6'}
@@ -7257,6 +7297,9 @@ packages:
     resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
     engines: {node: '>= 0.4'}
 
+  functional-red-black-tree@1.0.1:
+    resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
+
   functions-have-names@1.2.3:
     resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
 
@@ -7409,6 +7452,10 @@ packages:
     resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
     engines: {node: '>=14'}
 
+  google-gax@4.4.1:
+    resolution: {integrity: sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==}
+    engines: {node: '>=14'}
+
   google-logging-utils@0.0.2:
     resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
     engines: {node: '>=14'}
@@ -7624,6 +7671,10 @@ packages:
   http-cache-semantics@4.1.1:
     resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
 
+  http-proxy-agent@5.0.0:
+    resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
+    engines: {node: '>= 6'}
+
   http-proxy-agent@7.0.2:
     resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
     engines: {node: '>= 14'}
@@ -9738,6 +9789,10 @@ packages:
   property-information@6.5.0:
     resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
 
+  proto3-json-serializer@2.0.2:
+    resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==}
+    engines: {node: '>=14.0.0'}
+
   protobufjs@6.11.4:
     resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==}
     hasBin: true
@@ -10182,6 +10237,10 @@ packages:
     resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
     engines: {node: '>=18'}
 
+  retry-request@7.0.2:
+    resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==}
+    engines: {node: '>=14'}
+
   reusify@1.0.4:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -10585,6 +10644,12 @@ packages:
     resolution: {integrity: sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==}
     engines: {node: '>=0.10.0'}
 
+  stream-events@1.0.5:
+    resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==}
+
+  stream-shift@1.0.3:
+    resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
+
   stream-to-array@2.3.0:
     resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==}
 
@@ -10712,6 +10777,9 @@ packages:
     resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==}
     engines: {node: '>=16'}
 
+  stubs@3.0.0:
+    resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
+
   style-mod@4.1.2:
     resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
 
@@ -10821,6 +10889,10 @@ packages:
     resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
     engines: {node: '>=18'}
 
+  teeny-request@9.0.0:
+    resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==}
+    engines: {node: '>=14'}
+
   term-size@2.2.1:
     resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
     engines: {node: '>=8'}
@@ -13447,6 +13519,17 @@ snapshots:
       '@shikijs/types': 1.29.2
       '@shikijs/vscode-textmate': 10.0.1
 
+  '@google-cloud/firestore@7.11.0':
+    dependencies:
+      '@opentelemetry/api': 1.9.0
+      fast-deep-equal: 3.1.3
+      functional-red-black-tree: 1.0.1
+      google-gax: 4.4.1
+      protobufjs: 7.4.0
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+
   '@google-cloud/vertexai@1.9.0':
     dependencies:
       google-auth-library: 9.15.1
@@ -15661,6 +15744,8 @@ snapshots:
 
   '@tokenizer/token@0.3.0': {}
 
+  '@tootallnate/once@2.0.0': {}
+
   '@tootallnate/quickjs-emscripten@0.23.0': {}
 
   '@ts-graphviz/adapter@2.0.6':
@@ -15709,6 +15794,8 @@ snapshots:
     dependencies:
       '@babel/types': 7.26.8
 
+  '@types/caseless@0.12.5': {}
+
   '@types/cookie@0.6.0': {}
 
   '@types/debug@4.1.12':
@@ -15826,6 +15913,13 @@ snapshots:
       '@types/node': 22.9.0
       safe-buffer: 5.1.2
 
+  '@types/request@2.48.12':
+    dependencies:
+      '@types/caseless': 0.12.5
+      '@types/node': 22.9.0
+      '@types/tough-cookie': 4.0.5
+      form-data: 2.5.2
+
   '@types/resolve@1.20.2': {}
 
   '@types/statuses@2.0.5': {}
@@ -16351,7 +16445,6 @@ snapshots:
       debug: 4.4.0
     transitivePeerDependencies:
       - supports-color
-    optional: true
 
   agent-base@7.1.3: {}
 
@@ -17439,6 +17532,13 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  duplexify@4.1.3:
+    dependencies:
+      end-of-stream: 1.4.4
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+      stream-shift: 1.0.3
+
   eastasianwidth@0.2.0: {}
 
   ecdsa-sig-formatter@1.0.11:
@@ -18233,6 +18333,13 @@ snapshots:
 
   form-data-encoder@4.0.2: {}
 
+  form-data@2.5.2:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+      safe-buffer: 5.2.1
+
   form-data@4.0.0:
     dependencies:
       asynckit: 0.4.0
@@ -18511,6 +18618,8 @@ snapshots:
       hasown: 2.0.2
       is-callable: 1.2.7
 
+  functional-red-black-tree@1.0.1: {}
+
   functions-have-names@1.2.3: {}
 
   fuse.js@6.6.2: {}
@@ -18709,6 +18818,24 @@ snapshots:
       - encoding
       - supports-color
 
+  google-gax@4.4.1:
+    dependencies:
+      '@grpc/grpc-js': 1.12.6
+      '@grpc/proto-loader': 0.7.13
+      '@types/long': 4.0.2
+      abort-controller: 3.0.0
+      duplexify: 4.1.3
+      google-auth-library: 9.15.1
+      node-fetch: 2.7.0
+      object-hash: 3.0.0
+      proto3-json-serializer: 2.0.2
+      protobufjs: 7.4.0
+      retry-request: 7.0.2
+      uuid: 9.0.1
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+
   google-logging-utils@0.0.2: {}
 
   gopd@1.2.0: {}
@@ -19061,6 +19188,14 @@ snapshots:
 
   http-cache-semantics@4.1.1: {}
 
+  http-proxy-agent@5.0.0:
+    dependencies:
+      '@tootallnate/once': 2.0.0
+      agent-base: 6.0.2
+      debug: 4.4.0
+    transitivePeerDependencies:
+      - supports-color
+
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.3
@@ -19106,7 +19241,6 @@ snapshots:
       debug: 4.4.0
     transitivePeerDependencies:
       - supports-color
-    optional: true
 
   https-proxy-agent@7.0.6:
     dependencies:
@@ -21733,6 +21867,10 @@ snapshots:
 
   property-information@6.5.0: {}
 
+  proto3-json-serializer@2.0.2:
+    dependencies:
+      protobufjs: 7.4.0
+
   protobufjs@6.11.4:
     dependencies:
       '@protobufjs/aspromise': 1.1.2
@@ -22412,6 +22550,15 @@ snapshots:
       onetime: 7.0.0
       signal-exit: 4.1.0
 
+  retry-request@7.0.2:
+    dependencies:
+      '@types/request': 2.48.12
+      extend: 3.0.2
+      teeny-request: 9.0.0
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+
   reusify@1.0.4: {}
 
   rfdc@1.4.1: {}
@@ -22887,6 +23034,12 @@ snapshots:
 
   stopwords-iso@1.1.0: {}
 
+  stream-events@1.0.5:
+    dependencies:
+      stubs: 3.0.0
+
+  stream-shift@1.0.3: {}
+
   stream-to-array@2.3.0:
     dependencies:
       any-promise: 1.3.0
@@ -23046,6 +23199,8 @@ snapshots:
       '@tokenizer/token': 0.3.0
       peek-readable: 5.4.2
 
+  stubs@3.0.0: {}
+
   style-mod@4.1.2: {}
 
   style-to-object@0.4.4:
@@ -23223,6 +23378,17 @@ snapshots:
       mkdirp: 3.0.1
       yallist: 5.0.0
 
+  teeny-request@9.0.0:
+    dependencies:
+      http-proxy-agent: 5.0.0
+      https-proxy-agent: 5.0.1
+      node-fetch: 2.7.0
+      stream-events: 1.0.5
+      uuid: 9.0.1
+    transitivePeerDependencies:
+      - encoding
+      - supports-color
+
   term-size@2.2.1: {}
 
   terser-webpack-plugin@5.3.11(@swc/core@1.10.15(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.15(@swc/helpers@0.5.15))):
diff --git a/tsconfig.json b/tsconfig.json
index ea6733a72..a675b7b4b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -158,6 +158,9 @@
     {
       "path": "./packages/providers/storage/weaviate/tsconfig.json"
     },
+    {
+      "path": "./packages/providers/storage/firestore/tsconfig.json"
+    },
     {
       "path": "./packages/providers/google/tsconfig.json"
     },
-- 
GitLab