diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts
index d6ccf8265f..bb838bbae3 100644
--- a/e2e/src/api/specs/person.e2e-spec.ts
+++ b/e2e/src/api/specs/person.e2e-spec.ts
@@ -200,7 +200,7 @@ describe('/people', () => {
       expect(body).toMatchObject({
         id: expect.any(String),
         name: 'New Person',
-        birthDate: '1990-01-01',
+        birthDate: '1990-01-01T00:00:00.000Z',
       });
     });
   });
@@ -244,7 +244,7 @@ describe('/people', () => {
         .set('Authorization', `Bearer ${admin.accessToken}`)
         .send({ birthDate: '1990-01-01' });
       expect(status).toBe(200);
-      expect(body).toMatchObject({ birthDate: '1990-01-01' });
+      expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' });
     });
 
     it('should clear a date of birth', async () => {
diff --git a/machine-learning/app/models/clip/textual.py b/machine-learning/app/models/clip/textual.py
index 32c28ea2bb..d338f29296 100644
--- a/machine-learning/app/models/clip/textual.py
+++ b/machine-learning/app/models/clip/textual.py
@@ -10,7 +10,7 @@ from tokenizers import Encoding, Tokenizer
 
 from app.config import log
 from app.models.base import InferenceModel
-from app.models.transforms import clean_text
+from app.models.transforms import clean_text, serialize_np_array
 from app.schemas import ModelSession, ModelTask, ModelType
 
 
@@ -18,9 +18,9 @@ class BaseCLIPTextualEncoder(InferenceModel):
     depends = []
     identity = (ModelType.TEXTUAL, ModelTask.SEARCH)
 
-    def _predict(self, inputs: str, **kwargs: Any) -> NDArray[np.float32]:
+    def _predict(self, inputs: str, **kwargs: Any) -> str:
         res: NDArray[np.float32] = self.session.run(None, self.tokenize(inputs))[0][0]
-        return res
+        return serialize_np_array(res)
 
     def _load(self) -> ModelSession:
         session = super()._load()
diff --git a/machine-learning/app/models/clip/visual.py b/machine-learning/app/models/clip/visual.py
index 48058c961a..64be8e0657 100644
--- a/machine-learning/app/models/clip/visual.py
+++ b/machine-learning/app/models/clip/visual.py
@@ -10,7 +10,15 @@ from PIL import Image
 
 from app.config import log
 from app.models.base import InferenceModel
-from app.models.transforms import crop_pil, decode_pil, get_pil_resampling, normalize, resize_pil, to_numpy
+from app.models.transforms import (
+    crop_pil,
+    decode_pil,
+    get_pil_resampling,
+    normalize,
+    resize_pil,
+    serialize_np_array,
+    to_numpy,
+)
 from app.schemas import ModelSession, ModelTask, ModelType
 
 
@@ -18,10 +26,10 @@ class BaseCLIPVisualEncoder(InferenceModel):
     depends = []
     identity = (ModelType.VISUAL, ModelTask.SEARCH)
 
-    def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> NDArray[np.float32]:
+    def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> str:
         image = decode_pil(inputs)
         res: NDArray[np.float32] = self.session.run(None, self.transform(image))[0][0]
-        return res
+        return serialize_np_array(res)
 
     @abstractmethod
     def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
diff --git a/machine-learning/app/models/facial_recognition/recognition.py b/machine-learning/app/models/facial_recognition/recognition.py
index dcfb6b530e..044f19b06f 100644
--- a/machine-learning/app/models/facial_recognition/recognition.py
+++ b/machine-learning/app/models/facial_recognition/recognition.py
@@ -12,7 +12,7 @@ from PIL import Image
 
 from app.config import log, settings
 from app.models.base import InferenceModel
-from app.models.transforms import decode_cv2
+from app.models.transforms import decode_cv2, serialize_np_array
 from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType
 
 
@@ -61,7 +61,7 @@ class FaceRecognizer(InferenceModel):
         return [
             {
                 "boundingBox": {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
-                "embedding": embedding,
+                "embedding": serialize_np_array(embedding),
                 "score": score,
             }
             for (x1, y1, x2, y2), embedding, score in zip(faces["boxes"], embeddings, faces["scores"])
diff --git a/machine-learning/app/models/transforms.py b/machine-learning/app/models/transforms.py
index bb03103d4b..e70763a07f 100644
--- a/machine-learning/app/models/transforms.py
+++ b/machine-learning/app/models/transforms.py
@@ -4,6 +4,7 @@ from typing import IO
 
 import cv2
 import numpy as np
+import orjson
 from numpy.typing import NDArray
 from PIL import Image
 
@@ -69,3 +70,9 @@ def clean_text(text: str, canonicalize: bool = False) -> str:
     if canonicalize:
         text = text.translate(_PUNCTUATION_TRANS).lower()
     return text
+
+
+# this allows the client to use the array as a string without deserializing only to serialize back to a string
+# TODO: use this in a less invasive way
+def serialize_np_array(arr: NDArray[np.float32]) -> str:
+    return orjson.dumps(arr, option=orjson.OPT_SERIALIZE_NUMPY).decode()
diff --git a/machine-learning/app/schemas.py b/machine-learning/app/schemas.py
index a7ce2ee60d..d513faed6b 100644
--- a/machine-learning/app/schemas.py
+++ b/machine-learning/app/schemas.py
@@ -79,7 +79,7 @@ class FaceDetectionOutput(TypedDict):
 
 class DetectedFace(TypedDict):
     boundingBox: BoundingBox
-    embedding: npt.NDArray[np.float32]
+    embedding: str
     score: float
 
 
diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py
index 5da3baded7..b986f63668 100644
--- a/machine-learning/app/test_main.py
+++ b/machine-learning/app/test_main.py
@@ -10,6 +10,7 @@ from unittest import mock
 import cv2
 import numpy as np
 import onnxruntime as ort
+import orjson
 import pytest
 from fastapi import HTTPException
 from fastapi.testclient import TestClient
@@ -346,11 +347,11 @@ class TestCLIP:
         mocked.run.return_value = [[self.embedding]]
 
         clip_encoder = OpenClipVisualEncoder("ViT-B-32__openai", cache_dir="test_cache")
-        embedding = clip_encoder.predict(pil_image)
-
-        assert isinstance(embedding, np.ndarray)
-        assert embedding.shape[0] == clip_model_cfg["embed_dim"]
-        assert embedding.dtype == np.float32
+        embedding_str = clip_encoder.predict(pil_image)
+        assert isinstance(embedding_str, str)
+        embedding = orjson.loads(embedding_str)
+        assert isinstance(embedding, list)
+        assert len(embedding) == clip_model_cfg["embed_dim"]
         mocked.run.assert_called_once()
 
     def test_basic_text(
@@ -368,11 +369,11 @@ class TestCLIP:
         mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True)
 
         clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache")
-        embedding = clip_encoder.predict("test search query")
-
-        assert isinstance(embedding, np.ndarray)
-        assert embedding.shape[0] == clip_model_cfg["embed_dim"]
-        assert embedding.dtype == np.float32
+        embedding_str = clip_encoder.predict("test search query")
+        assert isinstance(embedding_str, str)
+        embedding = orjson.loads(embedding_str)
+        assert isinstance(embedding, list)
+        assert len(embedding) == clip_model_cfg["embed_dim"]
         mocked.run.assert_called_once()
 
     def test_openclip_tokenizer(
@@ -508,8 +509,11 @@ class TestFaceRecognition:
             assert isinstance(face.get("boundingBox"), dict)
             assert set(face["boundingBox"]) == {"x1", "y1", "x2", "y2"}
             assert all(isinstance(val, np.float32) for val in face["boundingBox"].values())
-            assert isinstance(face.get("embedding"), np.ndarray)
-            assert face["embedding"].shape[0] == 512
+            embedding_str = face.get("embedding")
+            assert isinstance(embedding_str, str)
+            embedding = orjson.loads(embedding_str)
+            assert isinstance(embedding, list)
+            assert len(embedding) == 512
             assert isinstance(face.get("score", None), np.float32)
 
         rec_model.get_feat.assert_called_once()
@@ -880,8 +884,10 @@ class TestPredictionEndpoints:
         actual = response.json()
         assert response.status_code == 200
         assert isinstance(actual, dict)
-        assert isinstance(actual.get("clip", None), list)
-        assert np.allclose(expected, actual["clip"])
+        embedding = actual.get("clip", None)
+        assert isinstance(embedding, str)
+        parsed_embedding = orjson.loads(embedding)
+        assert np.allclose(expected, parsed_embedding)
 
     def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
         expected = responses["clip"]["text"]
@@ -901,8 +907,10 @@ class TestPredictionEndpoints:
         actual = response.json()
         assert response.status_code == 200
         assert isinstance(actual, dict)
-        assert isinstance(actual.get("clip", None), list)
-        assert np.allclose(expected, actual["clip"])
+        embedding = actual.get("clip", None)
+        assert isinstance(embedding, str)
+        parsed_embedding = orjson.loads(embedding)
+        assert np.allclose(expected, parsed_embedding)
 
     def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
         byte_image = BytesIO()
@@ -933,5 +941,8 @@ class TestPredictionEndpoints:
 
         for expected_face, actual_face in zip(responses["facial-recognition"], actual["facial-recognition"]):
             assert expected_face["boundingBox"] == actual_face["boundingBox"]
-            assert np.allclose(expected_face["embedding"], actual_face["embedding"])
+            embedding = actual_face.get("embedding", None)
+            assert isinstance(embedding, str)
+            parsed_embedding = orjson.loads(embedding)
+            assert np.allclose(expected_face["embedding"], parsed_embedding)
             assert np.allclose(expected_face["score"], actual_face["score"])
diff --git a/server/src/decorators.ts b/server/src/decorators.ts
index 047b9ec4a7..bb037ee097 100644
--- a/server/src/decorators.ts
+++ b/server/src/decorators.ts
@@ -100,6 +100,7 @@ export const DummyValue = {
   DATE: new Date(),
   TIME_BUCKET: '2024-01-01T00:00:00.000Z',
   BOOLEAN: true,
+  VECTOR: '[1, 2, 3]',
 };
 
 export const GENERATE_SQL_KEY = 'generate-sql-key';
diff --git a/server/src/entities/face-search.entity.ts b/server/src/entities/face-search.entity.ts
index 2887453862..e907ba6c9e 100644
--- a/server/src/entities/face-search.entity.ts
+++ b/server/src/entities/face-search.entity.ts
@@ -11,10 +11,6 @@ export class FaceSearchEntity {
   faceId!: string;
 
   @Index('face_index', { synchronize: false })
-  @Column({
-    type: 'float4',
-    array: true,
-    transformer: { from: JSON.parse, to: (v) => `[${v}]` },
-  })
-  embedding!: number[];
+  @Column({ type: 'float4', array: true })
+  embedding!: string;
 }
diff --git a/server/src/entities/smart-search.entity.ts b/server/src/entities/smart-search.entity.ts
index 66017152ea..42245a17fb 100644
--- a/server/src/entities/smart-search.entity.ts
+++ b/server/src/entities/smart-search.entity.ts
@@ -11,6 +11,6 @@ export class SmartSearchEntity {
   assetId!: string;
 
   @Index('clip_index', { synchronize: false })
-  @Column({ type: 'float4', array: true, transformer: { from: JSON.parse, to: (v) => v } })
-  embedding!: number[];
+  @Column({ type: 'float4', array: true })
+  embedding!: string;
 }
diff --git a/server/src/interfaces/machine-learning.interface.ts b/server/src/interfaces/machine-learning.interface.ts
index 372aa0c7cd..934091ef8e 100644
--- a/server/src/interfaces/machine-learning.interface.ts
+++ b/server/src/interfaces/machine-learning.interface.ts
@@ -28,10 +28,10 @@ export type FaceDetectionOptions = ModelOptions & { minScore: number };
 
 type VisualResponse = { imageHeight: number; imageWidth: number };
 export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
-export type ClipVisualResponse = { [ModelTask.SEARCH]: number[] } & VisualResponse;
+export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
 
 export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
-export type ClipTextualResponse = { [ModelTask.SEARCH]: number[] };
+export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
 
 export type FacialRecognitionRequest = {
   [ModelTask.FACIAL_RECOGNITION]: {
@@ -42,7 +42,7 @@ export type FacialRecognitionRequest = {
 
 export interface Face {
   boundingBox: BoundingBox;
-  embedding: number[];
+  embedding: string;
   score: number;
 }
 
@@ -51,7 +51,7 @@ export type DetectedFaces = { faces: Face[] } & VisualResponse;
 export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
 
 export interface IMachineLearningRepository {
-  encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<number[]>;
-  encodeText(urls: string[], text: string, config: ModelOptions): Promise<number[]>;
+  encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<string>;
+  encodeText(urls: string[], text: string, config: ModelOptions): Promise<string>;
   detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
 }
diff --git a/server/src/interfaces/person.interface.ts b/server/src/interfaces/person.interface.ts
index dc89f5c1b0..d1404d829a 100644
--- a/server/src/interfaces/person.interface.ts
+++ b/server/src/interfaces/person.interface.ts
@@ -1,9 +1,10 @@
+import { Insertable, Updateable } from 'kysely';
+import { AssetFaces, FaceSearch, Person } from 'src/db';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
-import { FaceSearchEntity } from 'src/entities/face-search.entity';
 import { PersonEntity } from 'src/entities/person.entity';
 import { SourceType } from 'src/enum';
 import { Paginated, PaginationOptions } from 'src/utils/pagination';
-import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
+import { FindOptionsRelations } from 'typeorm';
 
 export const IPersonRepository = 'IPersonRepository';
 
@@ -48,29 +49,31 @@ export interface DeleteFacesOptions {
 
 export type UnassignFacesOptions = DeleteFacesOptions;
 
+export type SelectFaceOptions = Partial<{ [K in keyof AssetFaceEntity]: boolean }>;
+
 export interface IPersonRepository {
-  getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
+  getAll(options?: Partial<PersonEntity>): AsyncIterableIterator<PersonEntity>;
   getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
   getAllWithoutFaces(): Promise<PersonEntity[]>;
   getById(personId: string): Promise<PersonEntity | null>;
   getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
   getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
 
-  create(person: Partial<PersonEntity>): Promise<PersonEntity>;
-  createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
+  create(person: Insertable<Person>): Promise<PersonEntity>;
+  createAll(people: Insertable<Person>[]): Promise<string[]>;
   delete(entities: PersonEntity[]): Promise<void>;
   deleteFaces(options: DeleteFacesOptions): Promise<void>;
   refreshFaces(
-    facesToAdd: Partial<AssetFaceEntity>[],
+    facesToAdd: Insertable<AssetFaces>[],
     faceIdsToRemove: string[],
-    embeddingsToAdd?: FaceSearchEntity[],
+    embeddingsToAdd?: Insertable<FaceSearch>[],
   ): Promise<void>;
-  getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
+  getAllFaces(options?: Partial<AssetFaceEntity>): AsyncIterableIterator<AssetFaceEntity>;
   getFaceById(id: string): Promise<AssetFaceEntity>;
   getFaceByIdWithAssets(
     id: string,
     relations?: FindOptionsRelations<AssetFaceEntity>,
-    select?: FindOptionsSelect<AssetFaceEntity>,
+    select?: SelectFaceOptions,
   ): Promise<AssetFaceEntity | null>;
   getFaces(assetId: string): Promise<AssetFaceEntity[]>;
   getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
@@ -80,7 +83,7 @@ export interface IPersonRepository {
   getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
   reassignFaces(data: UpdateFacesData): Promise<number>;
   unassignFaces(options: UnassignFacesOptions): Promise<void>;
-  update(person: Partial<PersonEntity>): Promise<PersonEntity>;
-  updateAll(people: Partial<PersonEntity>[]): Promise<void>;
+  update(person: Updateable<Person> & { id: string }): Promise<PersonEntity>;
+  updateAll(people: Insertable<Person>[]): Promise<void>;
   getLatestFaceDate(): Promise<string | undefined>;
 }
diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts
index 0de8ef07d5..bb76ff7b1f 100644
--- a/server/src/interfaces/search.interface.ts
+++ b/server/src/interfaces/search.interface.ts
@@ -104,7 +104,7 @@ export interface SearchExifOptions {
 }
 
 export interface SearchEmbeddingOptions {
-  embedding: number[];
+  embedding: string;
   userIds: string[];
 }
 
@@ -152,7 +152,7 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
 
 export interface AssetDuplicateSearch {
   assetId: string;
-  embedding: number[];
+  embedding: string;
   maxDistance: number;
   type: AssetType;
   userIds: string[];
@@ -192,7 +192,7 @@ export interface ISearchRepository {
   searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
   searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
   searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
-  upsert(assetId: string, embedding: number[]): Promise<void>;
+  upsert(assetId: string, embedding: string): Promise<void>;
   searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
   getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
   deleteAllSearchEmbeddings(): Promise<void>;
diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql
index a7e683fca1..2c06d7c3f2 100644
--- a/server/src/queries/person.repository.sql
+++ b/server/src/queries/person.repository.sql
@@ -1,342 +1,252 @@
 -- NOTE: This file is auto generated by ./sql-generator
 
 -- PersonRepository.reassignFaces
-UPDATE "asset_faces"
-SET
+update "asset_faces"
+set
   "personId" = $1
-WHERE
-  "personId" = $2
+where
+  "asset_faces"."personId" = $2
 
--- PersonRepository.getAllForUser
-SELECT
-  "person"."id" AS "person_id",
-  "person"."createdAt" AS "person_createdAt",
-  "person"."updatedAt" AS "person_updatedAt",
-  "person"."ownerId" AS "person_ownerId",
-  "person"."name" AS "person_name",
-  "person"."birthDate" AS "person_birthDate",
-  "person"."thumbnailPath" AS "person_thumbnailPath",
-  "person"."faceAssetId" AS "person_faceAssetId",
-  "person"."isHidden" AS "person_isHidden"
-FROM
-  "person" "person"
-  INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
-  INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "person"."ownerId" = $1
-  AND "asset"."isArchived" = false
-  AND "person"."isHidden" = false
-GROUP BY
-  "person"."id"
-HAVING
-  "person"."name" != ''
-  OR COUNT("face"."assetId") >= $2
-ORDER BY
-  "person"."isHidden" ASC,
-  NULLIF("person"."name", '') IS NULL ASC,
-  COUNT("face"."assetId") DESC,
-  NULLIF("person"."name", '') ASC NULLS LAST,
-  "person"."createdAt" ASC
-LIMIT
-  11
-OFFSET
-  10
+-- PersonRepository.unassignFaces
+update "asset_faces"
+set
+  "personId" = $1
+where
+  "asset_faces"."sourceType" = $2
+VACUUM
+ANALYZE asset_faces,
+face_search,
+person
+REINDEX TABLE asset_faces
+REINDEX TABLE person
+
+-- PersonRepository.delete
+delete from "person"
+where
+  "person"."id" in ($1)
+
+-- PersonRepository.deleteFaces
+delete from "asset_faces"
+where
+  "asset_faces"."sourceType" = $1
+VACUUM
+ANALYZE asset_faces,
+face_search,
+person
+REINDEX TABLE asset_faces
+REINDEX TABLE person
 
 -- PersonRepository.getAllWithoutFaces
-SELECT
-  "person"."id" AS "person_id",
-  "person"."createdAt" AS "person_createdAt",
-  "person"."updatedAt" AS "person_updatedAt",
-  "person"."ownerId" AS "person_ownerId",
-  "person"."name" AS "person_name",
-  "person"."birthDate" AS "person_birthDate",
-  "person"."thumbnailPath" AS "person_thumbnailPath",
-  "person"."faceAssetId" AS "person_faceAssetId",
-  "person"."isHidden" AS "person_isHidden"
-FROM
-  "person" "person"
-  LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
-GROUP BY
+select
+  "person".*
+from
+  "person"
+  left join "asset_faces" on "asset_faces"."personId" = "person"."id"
+group by
   "person"."id"
-HAVING
-  COUNT("face"."assetId") = 0
+having
+  count("asset_faces"."assetId") = $1
 
 -- PersonRepository.getFaces
-SELECT
-  "AssetFaceEntity"."id" AS "AssetFaceEntity_id",
-  "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
-  "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
-  "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
-  "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
-  "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
-  "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
-  "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
-  "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
-  "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
-  "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
-  "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
-  "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
-  "AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
-  "AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
-  "AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
-  "AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
-  "AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
-  "AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
-FROM
-  "asset_faces" "AssetFaceEntity"
-  LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
-WHERE
-  (("AssetFaceEntity"."assetId" = $1))
-ORDER BY
-  "AssetFaceEntity"."boundingBoxX1" ASC
+select
+  "asset_faces".*,
+  (
+    select
+      to_json(obj)
+    from
+      (
+        select
+          "person".*
+        from
+          "person"
+        where
+          "person"."id" = "asset_faces"."personId"
+      ) as obj
+  ) as "person"
+from
+  "asset_faces"
+where
+  "asset_faces"."assetId" = $1
+order by
+  "asset_faces"."boundingBoxX1" asc
 
 -- PersonRepository.getFaceById
-SELECT DISTINCT
-  "distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
-FROM
+select
+  "asset_faces".*,
   (
-    SELECT
-      "AssetFaceEntity"."id" AS "AssetFaceEntity_id",
-      "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
-      "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
-      "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
-      "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
-      "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
-      "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
-      "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
-      "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
-      "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
-      "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
-      "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
-      "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
-      "AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
-      "AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
-      "AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
-      "AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
-      "AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
-      "AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
-    FROM
-      "asset_faces" "AssetFaceEntity"
-      LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
-    WHERE
-      (("AssetFaceEntity"."id" = $1))
-  ) "distinctAlias"
-ORDER BY
-  "AssetFaceEntity_id" ASC
-LIMIT
-  1
+    select
+      to_json(obj)
+    from
+      (
+        select
+          "person".*
+        from
+          "person"
+        where
+          "person"."id" = "asset_faces"."personId"
+      ) as obj
+  ) as "person"
+from
+  "asset_faces"
+where
+  "asset_faces"."id" = $1
 
 -- PersonRepository.getFaceByIdWithAssets
-SELECT DISTINCT
-  "distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
-FROM
+select
+  "asset_faces".*,
   (
-    SELECT
-      "AssetFaceEntity"."id" AS "AssetFaceEntity_id",
-      "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
-      "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
-      "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
-      "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
-      "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
-      "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
-      "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
-      "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
-      "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
-      "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
-      "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
-      "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
-      "AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
-      "AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
-      "AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
-      "AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
-      "AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
-      "AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
-      "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
-      "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
-      "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
-      "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
-      "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
-      "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
-      "AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
-      "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
-      "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
-      "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
-      "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
-      "AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
-      "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
-      "AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
-      "AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
-      "AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
-      "AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
-      "AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
-      "AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
-      "AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
-      "AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
-      "AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
-      "AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
-      "AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
-      "AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
-      "AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
-      "AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
-      "AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
-    FROM
-      "asset_faces" "AssetFaceEntity"
-      LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
-      LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
-      AND (
-        "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL
-      )
-    WHERE
-      (("AssetFaceEntity"."id" = $1))
-  ) "distinctAlias"
-ORDER BY
-  "AssetFaceEntity_id" ASC
-LIMIT
-  1
+    select
+      to_json(obj)
+    from
+      (
+        select
+          "person".*
+        from
+          "person"
+        where
+          "person"."id" = "asset_faces"."personId"
+      ) as obj
+  ) as "person",
+  (
+    select
+      to_json(obj)
+    from
+      (
+        select
+          "assets".*
+        from
+          "assets"
+        where
+          "assets"."id" = "asset_faces"."assetId"
+      ) as obj
+  ) as "asset"
+from
+  "asset_faces"
+where
+  "asset_faces"."id" = $1
 
 -- PersonRepository.reassignFace
-UPDATE "asset_faces"
-SET
+update "asset_faces"
+set
   "personId" = $1
-WHERE
-  "id" = $2
+where
+  "asset_faces"."id" = $2
 
 -- PersonRepository.getByName
-SELECT
-  "person"."id" AS "person_id",
-  "person"."createdAt" AS "person_createdAt",
-  "person"."updatedAt" AS "person_updatedAt",
-  "person"."ownerId" AS "person_ownerId",
-  "person"."name" AS "person_name",
-  "person"."birthDate" AS "person_birthDate",
-  "person"."thumbnailPath" AS "person_thumbnailPath",
-  "person"."faceAssetId" AS "person_faceAssetId",
-  "person"."isHidden" AS "person_isHidden"
-FROM
-  "person" "person"
-WHERE
-  "person"."ownerId" = $1
-  AND (
-    LOWER("person"."name") LIKE $2
-    OR LOWER("person"."name") LIKE $3
-  )
-LIMIT
-  1000
-
--- PersonRepository.getDistinctNames
-SELECT DISTINCT
-  ON (lower("person"."name")) "person"."id" AS "person_id",
-  "person"."name" AS "person_name"
-FROM
-  "person" "person"
-WHERE
-  "person"."ownerId" = $1
-  AND "person"."name" != ''
-
--- PersonRepository.getStatistics
-SELECT
-  COUNT(DISTINCT ("asset"."id")) AS "count"
-FROM
-  "asset_faces" "face"
-  LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "face"."personId" = $1
-  AND "asset"."isArchived" = false
-  AND "asset"."deletedAt" IS NULL
-  AND "asset"."livePhotoVideoId" IS NULL
-
--- PersonRepository.getNumberOfPeople
-SELECT
-  COUNT(DISTINCT ("person"."id")) AS "total",
-  COUNT(DISTINCT ("person"."id")) FILTER (
-    WHERE
-      "person"."isHidden" = true
-  ) AS "hidden"
-FROM
-  "person" "person"
-  INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
-  INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
-  AND ("asset"."deletedAt" IS NULL)
-WHERE
-  "person"."ownerId" = $1
-  AND "asset"."isArchived" = false
-
--- PersonRepository.getFacesByIds
-SELECT
-  "AssetFaceEntity"."id" AS "AssetFaceEntity_id",
-  "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
-  "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
-  "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
-  "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
-  "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
-  "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
-  "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
-  "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
-  "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
-  "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
-  "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
-  "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
-  "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
-  "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
-  "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
-  "AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
-  "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
-  "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
-  "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
-  "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
-  "AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
-  "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
-  "AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
-  "AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
-  "AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
-  "AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
-  "AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
-  "AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
-  "AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
-  "AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
-  "AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
-  "AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
-  "AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
-  "AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
-  "AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
-  "AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
-  "AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
-FROM
-  "asset_faces" "AssetFaceEntity"
-  LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
-WHERE
+select
+  "person".*
+from
+  "person"
+where
   (
-    (
-      (
-        ("AssetFaceEntity"."assetId" = $1)
-        AND ("AssetFaceEntity"."personId" = $2)
-      )
+    "person"."ownerId" = $1
+    and (
+      lower("person"."name") like $2
+      or lower("person"."name") like $3
     )
   )
+limit
+  $4
+
+-- PersonRepository.getDistinctNames
+select distinct
+  on (lower("person"."name")) "person"."id",
+  "person"."name"
+from
+  "person"
+where
+  (
+    "person"."ownerId" = $1
+    and "person"."name" != $2
+  )
+
+-- PersonRepository.getStatistics
+select
+  count(distinct ("assets"."id")) as "count"
+from
+  "asset_faces"
+  left join "assets" on "assets"."id" = "asset_faces"."assetId"
+  and "asset_faces"."personId" = $1
+  and "assets"."isArchived" = $2
+  and "assets"."deletedAt" is null
+  and "assets"."livePhotoVideoId" is null
+
+-- PersonRepository.getNumberOfPeople
+select
+  count(distinct ("person"."id")) as "total",
+  count(distinct ("person"."id")) filter (
+    where
+      "person"."isHidden" = $1
+  ) as "hidden"
+from
+  "person"
+  inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
+  inner join "assets" on "assets"."id" = "asset_faces"."assetId"
+  and "assets"."deletedAt" is null
+  and "assets"."isArchived" = $2
+where
+  "person"."ownerId" = $3
+
+-- PersonRepository.refreshFaces
+with
+  "added_embeddings" as (
+    insert into
+      "face_search" ("faceId", "embedding")
+    values
+      ($1, $2)
+  )
+select
+from
+  (
+    select
+      1
+  ) as "dummy"
+
+-- PersonRepository.getFacesByIds
+select
+  "asset_faces".*,
+  (
+    select
+      to_json(obj)
+    from
+      (
+        select
+          "assets".*
+        from
+          "assets"
+        where
+          "assets"."id" = "asset_faces"."assetId"
+      ) as obj
+  ) as "asset",
+  (
+    select
+      to_json(obj)
+    from
+      (
+        select
+          "person".*
+        from
+          "person"
+        where
+          "person"."id" = "asset_faces"."personId"
+      ) as obj
+  ) as "person"
+from
+  "asset_faces"
+where
+  "asset_faces"."assetId" in ($1)
+  and "asset_faces"."personId" in ($2)
 
 -- PersonRepository.getRandomFace
-SELECT
-  "AssetFaceEntity"."id" AS "AssetFaceEntity_id",
-  "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
-  "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
-  "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
-  "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
-  "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
-  "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
-  "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
-  "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
-  "AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType"
-FROM
-  "asset_faces" "AssetFaceEntity"
-WHERE
-  (("AssetFaceEntity"."personId" = $1))
-LIMIT
-  1
+select
+  "asset_faces".*
+from
+  "asset_faces"
+where
+  "asset_faces"."personId" = $1
 
 -- PersonRepository.getLatestFaceDate
-SELECT
-  MAX("jobStatus"."facesRecognizedAt")::text AS "latestDate"
-FROM
-  "asset_job_status" "jobStatus"
+select
+  max("asset_job_status"."facesRecognizedAt")::text as "latestDate"
+from
+  "asset_job_status"
diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql
index a6e93bd480..784babfc02 100644
--- a/server/src/queries/search.repository.sql
+++ b/server/src/queries/search.repository.sql
@@ -76,7 +76,7 @@ where
   and "assets"."isArchived" = $5
   and "assets"."deletedAt" is null
 order by
-  smart_search.embedding <= > $6::vector
+  smart_search.embedding <= > $6
 limit
   $7
 offset
@@ -88,7 +88,7 @@ with
     select
       "assets"."id" as "assetId",
       "assets"."duplicateId",
-      smart_search.embedding <= > $1::vector as "distance"
+      smart_search.embedding <= > $1 as "distance"
     from
       "assets"
       inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
@@ -99,7 +99,7 @@ with
       and "assets"."type" = $4
       and "assets"."id" != $5::uuid
     order by
-      smart_search.embedding <= > $6::vector
+      smart_search.embedding <= > $6
     limit
       $7
   )
@@ -116,7 +116,7 @@ with
     select
       "asset_faces"."id",
       "asset_faces"."personId",
-      face_search.embedding <= > $1::vector as "distance"
+      face_search.embedding <= > $1 as "distance"
     from
       "asset_faces"
       inner join "assets" on "assets"."id" = "asset_faces"."assetId"
@@ -125,7 +125,7 @@ with
       "assets"."ownerId" = any ($2::uuid [])
       and "assets"."deletedAt" is null
     order by
-      face_search.embedding <= > $3::vector
+      face_search.embedding <= > $3
     limit
       $4
   )
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index 4229286706..c810b0def2 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -1,13 +1,13 @@
 import { Injectable } from '@nestjs/common';
-import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
+import { ExpressionBuilder, Insertable, Kysely, SelectExpression, sql } from 'kysely';
+import { jsonObjectFrom } from 'kysely/helpers/postgres';
 import _ from 'lodash';
+import { InjectKysely } from 'nestjs-kysely';
+import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
 import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
-import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
-import { AssetEntity } from 'src/entities/asset.entity';
-import { FaceSearchEntity } from 'src/entities/face-search.entity';
 import { PersonEntity } from 'src/entities/person.entity';
-import { PaginationMode, SourceType } from 'src/enum';
+import { SourceType } from 'src/enum';
 import {
   AssetFaceId,
   DeleteFacesOptions,
@@ -17,332 +17,418 @@ import {
   PersonNameSearchOptions,
   PersonSearchOptions,
   PersonStatistics,
+  SelectFaceOptions,
   UnassignFacesOptions,
   UpdateFacesData,
 } from 'src/interfaces/person.interface';
-import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
-import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
+import { mapUpsertColumns } from 'src/utils/database';
+import { Paginated, PaginationOptions } from 'src/utils/pagination';
+import { FindOptionsRelations } from 'typeorm';
+
+const withPerson = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
+  return jsonObjectFrom(
+    eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_faces.personId'),
+  ).as('person');
+};
+
+const withAsset = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
+  return jsonObjectFrom(
+    eb.selectFrom('assets').selectAll('assets').whereRef('assets.id', '=', 'asset_faces.assetId'),
+  ).as('asset');
+};
+
+const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
+  return jsonObjectFrom(
+    eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_faces.id'),
+  ).as('faceSearch');
+};
 
 @Injectable()
 export class PersonRepository implements IPersonRepository {
-  constructor(
-    @InjectDataSource() private dataSource: DataSource,
-    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
-    @InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
-    @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
-    @InjectRepository(FaceSearchEntity) private faceSearchRepository: Repository<FaceSearchEntity>,
-    @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
-  ) {}
+  constructor(@InjectKysely() private db: Kysely<DB>) {}
 
   @GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
   async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
-    const result = await this.assetFaceRepository
-      .createQueryBuilder()
-      .update()
+    const result = await this.db
+      .updateTable('asset_faces')
       .set({ personId: newPersonId })
-      .where(_.omitBy({ personId: oldPersonId, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
-      .execute();
+      .$if(!!oldPersonId, (qb) => qb.where('asset_faces.personId', '=', oldPersonId!))
+      .$if(!!faceIds, (qb) => qb.where('asset_faces.id', 'in', faceIds!))
+      .executeTakeFirst();
 
-    return result.affected ?? 0;
+    return Number(result.numChangedRows) ?? 0;
   }
 
+  @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
   async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
-    await this.assetFaceRepository
-      .createQueryBuilder()
-      .update()
+    await this.db
+      .updateTable('asset_faces')
       .set({ personId: null })
-      .where({ sourceType })
+      .where('asset_faces.sourceType', '=', sourceType)
       .execute();
 
     await this.vacuum({ reindexVectors: false });
   }
 
+  @GenerateSql({ params: [[{ id: DummyValue.UUID }]] })
   async delete(entities: PersonEntity[]): Promise<void> {
-    await this.personRepository.remove(entities);
+    if (entities.length === 0) {
+      return;
+    }
+
+    await this.db
+      .deleteFrom('person')
+      .where(
+        'person.id',
+        'in',
+        entities.map(({ id }) => id),
+      )
+      .execute();
   }
 
+  @GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
   async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
-    await this.assetFaceRepository
-      .createQueryBuilder('asset_faces')
-      .delete()
-      .andWhere('sourceType = :sourceType', { sourceType })
-      .execute();
+    await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
 
     await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
   }
 
-  getAllFaces(
-    pagination: PaginationOptions,
-    options: FindManyOptions<AssetFaceEntity> = {},
-  ): Paginated<AssetFaceEntity> {
-    return paginate(this.assetFaceRepository, pagination, options);
+  getAllFaces(options: Partial<AssetFaceEntity> = {}): AsyncIterableIterator<AssetFaceEntity> {
+    return this.db
+      .selectFrom('asset_faces')
+      .selectAll('asset_faces')
+      .$if(options.personId === null, (qb) => qb.where('asset_faces.personId', 'is', null))
+      .$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!))
+      .$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!))
+      .$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
+      .$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
+      .stream() as AsyncIterableIterator<AssetFaceEntity>;
   }
 
-  getAll(pagination: PaginationOptions, options: FindManyOptions<PersonEntity> = {}): Paginated<PersonEntity> {
-    return paginate(this.personRepository, pagination, options);
+  getAll(options: Partial<PersonEntity> = {}): AsyncIterableIterator<PersonEntity> {
+    return this.db
+      .selectFrom('person')
+      .selectAll('person')
+      .$if(!!options.ownerId, (qb) => qb.where('person.ownerId', '=', options.ownerId!))
+      .$if(!!options.thumbnailPath, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
+      .$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null))
+      .$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!))
+      .$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!))
+      .stream() as AsyncIterableIterator<PersonEntity>;
   }
 
-  @GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] })
   async getAllForUser(
     pagination: PaginationOptions,
     userId: string,
     options?: PersonSearchOptions,
   ): Paginated<PersonEntity> {
-    const queryBuilder = this.personRepository
-      .createQueryBuilder('person')
-      .innerJoin('person.faces', 'face')
-      .where('person.ownerId = :userId', { userId })
-      .innerJoin('face.asset', 'asset')
-      .andWhere('asset.isArchived = false')
-      .orderBy('person.isHidden', 'ASC')
-      .addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
-      .addOrderBy('COUNT(face.assetId)', 'DESC')
-      .addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
-      .addOrderBy('person.createdAt')
-      .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 })
-      .groupBy('person.id');
-    if (options?.closestFaceAssetId) {
-      const innerQueryBuilder = this.faceSearchRepository
-        .createQueryBuilder('face_search')
-        .select('embedding', 'embedding')
-        .where('"face_search"."faceId" = "person"."faceAssetId"');
-      const faceSelectQueryBuilder = this.faceSearchRepository
-        .createQueryBuilder('face_search')
-        .select('embedding', 'embedding')
-        .where('"face_search"."faceId" = :faceId', { faceId: options.closestFaceAssetId });
-      queryBuilder
-        .orderBy('(' + innerQueryBuilder.getQuery() + ') <=> (' + faceSelectQueryBuilder.getQuery() + ')')
-        .setParameters(faceSelectQueryBuilder.getParameters());
+    const items = (await this.db
+      .selectFrom('person')
+      .selectAll('person')
+      .innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
+      .innerJoin('assets', (join) =>
+        join
+          .onRef('asset_faces.assetId', '=', 'assets.id')
+          .on('assets.isArchived', '=', false)
+          .on('assets.deletedAt', 'is', null),
+      )
+      .where('person.ownerId', '=', userId)
+      .orderBy('person.isHidden', 'asc')
+      .orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
+      .orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
+      .orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
+      .orderBy('person.createdAt')
+      .having((eb) =>
+        eb.or([
+          eb('person.name', '!=', ''),
+          eb((innerEb) => innerEb.fn.count('asset_faces.assetId'), '>=', options?.minimumFaceCount || 1),
+        ]),
+      )
+      .groupBy('person.id')
+      .$if(!!options?.closestFaceAssetId, (qb) =>
+        qb.orderBy((eb) =>
+          eb(
+            (eb) =>
+              eb
+                .selectFrom('face_search')
+                .select('face_search.embedding')
+                .whereRef('face_search.faceId', '=', 'person.faceAssetId'),
+            '<=>',
+            (eb) =>
+              eb
+                .selectFrom('face_search')
+                .select('face_search.embedding')
+                .where('face_search.faceId', '=', options!.closestFaceAssetId!),
+          ),
+        ),
+      )
+      .$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
+      .offset(pagination.skip ?? 0)
+      .limit(pagination.take + 1)
+      .execute()) as PersonEntity[];
+
+    if (items.length > pagination.take) {
+      return { items: items.slice(0, -1), hasNextPage: true };
     }
-    if (!options?.withHidden) {
-      queryBuilder.andWhere('person.isHidden = false');
-    }
-    return paginatedBuilder(queryBuilder, {
-      mode: PaginationMode.LIMIT_OFFSET,
-      ...pagination,
-    });
+
+    return { items, hasNextPage: false };
   }
 
   @GenerateSql()
   getAllWithoutFaces(): Promise<PersonEntity[]> {
-    return this.personRepository
-      .createQueryBuilder('person')
-      .leftJoin('person.faces', 'face')
-      .having('COUNT(face.assetId) = 0')
+    return this.db
+      .selectFrom('person')
+      .selectAll('person')
+      .leftJoin('asset_faces', 'asset_faces.personId', 'person.id')
+      .having((eb) => eb.fn.count('asset_faces.assetId'), '=', 0)
       .groupBy('person.id')
-      .withDeleted()
-      .getMany();
+      .execute() as Promise<PersonEntity[]>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
   getFaces(assetId: string): Promise<AssetFaceEntity[]> {
-    return this.assetFaceRepository.find({
-      where: { assetId },
-      relations: {
-        person: true,
-      },
-      order: {
-        boundingBoxX1: 'ASC',
-      },
-    });
+    return this.db
+      .selectFrom('asset_faces')
+      .selectAll('asset_faces')
+      .select(withPerson)
+      .where('asset_faces.assetId', '=', assetId)
+      .orderBy('asset_faces.boundingBoxX1', 'asc')
+      .execute() as Promise<AssetFaceEntity[]>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
   getFaceById(id: string): Promise<AssetFaceEntity> {
     // TODO return null instead of find or fail
-    return this.assetFaceRepository.findOneOrFail({
-      where: { id },
-      relations: {
-        person: true,
-      },
-    });
+    return this.db
+      .selectFrom('asset_faces')
+      .selectAll('asset_faces')
+      .select(withPerson)
+      .where('asset_faces.id', '=', id)
+      .executeTakeFirstOrThrow() as Promise<AssetFaceEntity>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
   getFaceByIdWithAssets(
     id: string,
-    relations: FindOptionsRelations<AssetFaceEntity>,
-    select: FindOptionsSelect<AssetFaceEntity>,
+    relations?: FindOptionsRelations<AssetFaceEntity>,
+    select?: SelectFaceOptions,
   ): Promise<AssetFaceEntity | null> {
-    return this.assetFaceRepository.findOne(
-      _.omitBy(
-        {
-          where: { id },
-          relations: {
-            ...relations,
-            person: true,
-            asset: true,
-          },
-          select,
-        },
-        _.isUndefined,
-      ),
-    );
+    return (this.db
+      .selectFrom('asset_faces')
+      .$if(!!select, (qb) =>
+        qb.select(
+          Object.keys(
+            _.omitBy({ ...select!, faceSearch: undefined, asset: undefined }, _.isUndefined),
+          ) as SelectExpression<DB, 'asset_faces'>[],
+        ),
+      )
+      .$if(!select, (qb) => qb.selectAll('asset_faces'))
+      .select(withPerson)
+      .select(withAsset)
+      .$if(!!relations?.faceSearch, (qb) => qb.select(withFaceSearch))
+      .where('asset_faces.id', '=', id)
+      .executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
   async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
-    const result = await this.assetFaceRepository
-      .createQueryBuilder()
-      .update()
+    const result = await this.db
+      .updateTable('asset_faces')
       .set({ personId: newPersonId })
-      .where({ id: assetFaceId })
-      .execute();
+      .where('asset_faces.id', '=', assetFaceId)
+      .executeTakeFirst();
 
-    return result.affected ?? 0;
+    return Number(result.numChangedRows) ?? 0;
   }
 
   getById(personId: string): Promise<PersonEntity | null> {
-    return this.personRepository.findOne({ where: { id: personId } });
+    return (this.db //
+      .selectFrom('person')
+      .selectAll('person')
+      .where('person.id', '=', personId)
+      .executeTakeFirst() ?? null) as Promise<PersonEntity | null>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
   getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise<PersonEntity[]> {
-    const queryBuilder = this.personRepository
-      .createQueryBuilder('person')
-      .where(
-        'person.ownerId = :userId AND (LOWER(person.name) LIKE :nameStart OR LOWER(person.name) LIKE :nameAnywhere)',
-        { userId, nameStart: `${personName.toLowerCase()}%`, nameAnywhere: `% ${personName.toLowerCase()}%` },
+    return this.db
+      .selectFrom('person')
+      .selectAll('person')
+      .where((eb) =>
+        eb.and([
+          eb('person.ownerId', '=', userId),
+          eb.or([
+            eb(eb.fn('lower', ['person.name']), 'like', `${personName.toLowerCase()}%`),
+            eb(eb.fn('lower', ['person.name']), 'like', `% ${personName.toLowerCase()}%`),
+          ]),
+        ]),
       )
-      .limit(1000);
-
-    if (!withHidden) {
-      queryBuilder.andWhere('person.isHidden = false');
-    }
-    return queryBuilder.getMany();
+      .limit(1000)
+      .$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
+      .execute() as Promise<PersonEntity[]>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] })
   getDistinctNames(userId: string, { withHidden }: PersonNameSearchOptions): Promise<PersonNameResponse[]> {
-    const queryBuilder = this.personRepository
-      .createQueryBuilder('person')
+    return this.db
+      .selectFrom('person')
       .select(['person.id', 'person.name'])
-      .distinctOn(['lower(person.name)'])
-      .where(`person.ownerId = :userId AND person.name != ''`, { userId });
-
-    if (!withHidden) {
-      queryBuilder.andWhere('person.isHidden = false');
-    }
-
-    return queryBuilder.getMany();
+      .distinctOn((eb) => eb.fn('lower', ['person.name']))
+      .where((eb) => eb.and([eb('person.ownerId', '=', userId), eb('person.name', '!=', '')]))
+      .$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
+      .execute();
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
   async getStatistics(personId: string): Promise<PersonStatistics> {
-    const items = await this.assetFaceRepository
-      .createQueryBuilder('face')
-      .leftJoin('face.asset', 'asset')
-      .where('face.personId = :personId', { personId })
-      .andWhere('asset.isArchived = false')
-      .andWhere('asset.deletedAt IS NULL')
-      .andWhere('asset.livePhotoVideoId IS NULL')
-      .select('COUNT(DISTINCT(asset.id))', 'count')
-      .getRawOne();
+    const result = await this.db
+      .selectFrom('asset_faces')
+      .leftJoin('assets', (join) =>
+        join
+          .onRef('assets.id', '=', 'asset_faces.assetId')
+          .on('asset_faces.personId', '=', personId)
+          .on('assets.isArchived', '=', false)
+          .on('assets.deletedAt', 'is', null)
+          .on('assets.livePhotoVideoId', 'is', null),
+      )
+      .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
+      .executeTakeFirst();
+
     return {
-      assets: items.count ?? 0,
+      assets: result ? Number(result.count) : 0,
     };
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
   async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
-    const items = await this.personRepository
-      .createQueryBuilder('person')
-      .innerJoin('person.faces', 'face')
-      .where('person.ownerId = :userId', { userId })
-      .innerJoin('face.asset', 'asset')
-      .andWhere('asset.isArchived = false')
-      .select('COUNT(DISTINCT(person.id))', 'total')
-      .addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden')
-      .getRawOne();
+    const items = await this.db
+      .selectFrom('person')
+      .innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
+      .where('person.ownerId', '=', userId)
+      .innerJoin('assets', (join) =>
+        join
+          .onRef('assets.id', '=', 'asset_faces.assetId')
+          .on('assets.deletedAt', 'is', null)
+          .on('assets.isArchived', '=', false),
+      )
+      .select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
+      .select((eb) =>
+        eb.fn
+          .count(eb.fn('distinct', ['person.id']))
+          .filterWhere('person.isHidden', '=', true)
+          .as('hidden'),
+      )
+      .executeTakeFirst();
 
     if (items == undefined) {
       return { total: 0, hidden: 0 };
     }
 
-    const result: PeopleStatistics = {
-      total: items.total ?? 0,
-      hidden: items.hidden ?? 0,
+    return {
+      total: Number(items.total),
+      hidden: Number(items.hidden),
     };
-
-    return result;
   }
 
-  create(person: Partial<PersonEntity>): Promise<PersonEntity> {
-    return this.save(person);
+  create(person: Insertable<Person>): Promise<PersonEntity> {
+    return this.db.insertInto('person').values(person).returningAll().executeTakeFirst() as Promise<PersonEntity>;
   }
 
-  async createAll(people: Partial<PersonEntity>[]): Promise<string[]> {
-    const results = await this.personRepository.save(people);
-    return results.map((person) => person.id);
+  async createAll(people: Insertable<Person>[]): Promise<string[]> {
+    const results = await this.db.insertInto('person').values(people).returningAll().execute();
+    return results.map(({ id }) => id);
   }
 
+  @GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] })
   async refreshFaces(
-    facesToAdd: Partial<AssetFaceEntity>[],
+    facesToAdd: (Insertable<AssetFaces> & { assetId: string })[],
     faceIdsToRemove: string[],
-    embeddingsToAdd?: FaceSearchEntity[],
+    embeddingsToAdd?: Insertable<FaceSearch>[],
   ): Promise<void> {
-    const query = this.faceSearchRepository.createQueryBuilder().select('1').fromDummy();
+    let query = this.db;
     if (facesToAdd.length > 0) {
-      const insertCte = this.assetFaceRepository.createQueryBuilder().insert().values(facesToAdd);
-      query.addCommonTableExpression(insertCte, 'added');
+      (query as any) = query.with('added', (db) => db.insertInto('asset_faces').values(facesToAdd));
     }
 
     if (faceIdsToRemove.length > 0) {
-      const deleteCte = this.assetFaceRepository
-        .createQueryBuilder()
-        .delete()
-        .where('id = any(:faceIdsToRemove)', { faceIdsToRemove });
-      query.addCommonTableExpression(deleteCte, 'deleted');
+      (query as any) = query.with('removed', (db) =>
+        db.deleteFrom('asset_faces').where('asset_faces.id', '=', (eb) => eb.fn.any(eb.val(faceIdsToRemove))),
+      );
     }
 
     if (embeddingsToAdd?.length) {
-      const embeddingCte = this.faceSearchRepository.createQueryBuilder().insert().values(embeddingsToAdd).orIgnore();
-      query.addCommonTableExpression(embeddingCte, 'embeddings');
-      query.getQuery(); // typeorm mixes up parameters without this
+      (query as any) = query.with('added_embeddings', (db) => db.insertInto('face_search').values(embeddingsToAdd));
     }
 
-    await query.execute();
+    await query.selectFrom(sql`(select 1)`.as('dummy')).execute();
   }
 
-  async update(person: Partial<PersonEntity>): Promise<PersonEntity> {
-    return this.save(person);
+  async update(person: Partial<PersonEntity> & { id: string }): Promise<PersonEntity> {
+    return this.db
+      .updateTable('person')
+      .set(person)
+      .where('person.id', '=', person.id)
+      .returningAll()
+      .executeTakeFirstOrThrow() as Promise<PersonEntity>;
   }
 
-  async updateAll(people: Partial<PersonEntity>[]): Promise<void> {
-    await this.personRepository.save(people);
+  async updateAll(people: Insertable<Person>[]): Promise<void> {
+    if (people.length === 0) {
+      return;
+    }
+
+    await this.db
+      .insertInto('person')
+      .values(people)
+      .onConflict((oc) => oc.column('id').doUpdateSet(() => mapUpsertColumns('person', people[0], ['id'])))
+      .execute();
   }
 
   @GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
   @ChunkedArray()
-  async getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
-    return this.assetFaceRepository.find({ where: ids, relations: { asset: true }, withDeleted: true });
+  getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
+    const { assetIds, personIds }: { assetIds: string[]; personIds: string[] } = { assetIds: [], personIds: [] };
+
+    for (const { assetId, personId } of ids) {
+      assetIds.push(assetId);
+      personIds.push(personId);
+    }
+
+    return this.db
+      .selectFrom('asset_faces')
+      .selectAll('asset_faces')
+      .select(withAsset)
+      .select(withPerson)
+      .where('asset_faces.assetId', 'in', assetIds)
+      .where('asset_faces.personId', 'in', personIds)
+      .execute() as Promise<AssetFaceEntity[]>;
   }
 
   @GenerateSql({ params: [DummyValue.UUID] })
-  async getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
-    return this.assetFaceRepository.findOneBy({ personId });
+  getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
+    return (this.db
+      .selectFrom('asset_faces')
+      .selectAll('asset_faces')
+      .where('asset_faces.personId', '=', personId)
+      .executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
   }
 
   @GenerateSql()
   async getLatestFaceDate(): Promise<string | undefined> {
-    const result: { latestDate?: string } | undefined = await this.jobStatusRepository
-      .createQueryBuilder('jobStatus')
-      .select('MAX(jobStatus.facesRecognizedAt)::text', 'latestDate')
-      .getRawOne();
+    const result = (await this.db
+      .selectFrom('asset_job_status')
+      .select((eb) => sql`${eb.fn.max('asset_job_status.facesRecognizedAt')}::text`.as('latestDate'))
+      .executeTakeFirst()) as { latestDate: string } | undefined;
+
     return result?.latestDate;
   }
 
-  private async save(person: Partial<PersonEntity>): Promise<PersonEntity> {
-    const { id } = await this.personRepository.save(person);
-    return this.personRepository.findOneByOrFail({ id });
-  }
-
   private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
-    await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search, person');
-    await this.assetFaceRepository.query('REINDEX TABLE asset_faces');
-    await this.assetFaceRepository.query('REINDEX TABLE person');
+    await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db);
+    await sql`REINDEX TABLE asset_faces`.execute(this.db);
+    await sql`REINDEX TABLE person`.execute(this.db);
     if (reindexVectors) {
-      await this.assetFaceRepository.query('REINDEX TABLE face_search');
+      await sql`REINDEX TABLE face_search`.execute(this.db);
     }
   }
 }
diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts
index 0c01f3409d..0e43063e9a 100644
--- a/server/src/repositories/search.repository.ts
+++ b/server/src/repositories/search.repository.ts
@@ -20,7 +20,7 @@ import {
   SearchPaginationOptions,
   SmartSearchOptions,
 } from 'src/interfaces/search.interface';
-import { anyUuid, asUuid, asVector } from 'src/utils/database';
+import { anyUuid, asUuid } from 'src/utils/database';
 import { Paginated } from 'src/utils/pagination';
 import { isValidInteger } from 'src/validation';
 
@@ -82,7 +82,7 @@ export class SearchRepository implements ISearchRepository {
       { page: 1, size: 200 },
       {
         takenAfter: DummyValue.DATE,
-        embedding: Array.from({ length: 512 }, Math.random),
+        embedding: DummyValue.VECTOR,
         lensModel: DummyValue.STRING,
         withStacked: true,
         isFavorite: true,
@@ -97,7 +97,7 @@ export class SearchRepository implements ISearchRepository {
 
     const items = (await searchAssetBuilder(this.db, options)
       .innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
-      .orderBy(sql`smart_search.embedding <=> ${asVector(options.embedding)}`)
+      .orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
       .limit(pagination.size + 1)
       .offset((pagination.page - 1) * pagination.size)
       .execute()) as any as AssetEntity[];
@@ -111,7 +111,7 @@ export class SearchRepository implements ISearchRepository {
     params: [
       {
         assetId: DummyValue.UUID,
-        embedding: Array.from({ length: 512 }, Math.random),
+        embedding: DummyValue.VECTOR,
         maxDistance: 0.6,
         type: AssetType.IMAGE,
         userIds: [DummyValue.UUID],
@@ -119,7 +119,6 @@ export class SearchRepository implements ISearchRepository {
     ],
   })
   searchDuplicates({ assetId, embedding, maxDistance, type, userIds }: AssetDuplicateSearch) {
-    const vector = asVector(embedding);
     return this.db
       .with('cte', (qb) =>
         qb
@@ -127,7 +126,7 @@ export class SearchRepository implements ISearchRepository {
           .select([
             'assets.id as assetId',
             'assets.duplicateId',
-            sql<number>`smart_search.embedding <=> ${vector}`.as('distance'),
+            sql<number>`smart_search.embedding <=> ${embedding}`.as('distance'),
           ])
           .innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
           .where('assets.ownerId', '=', anyUuid(userIds))
@@ -135,7 +134,7 @@ export class SearchRepository implements ISearchRepository {
           .where('assets.isVisible', '=', true)
           .where('assets.type', '=', type)
           .where('assets.id', '!=', asUuid(assetId))
-          .orderBy(sql`smart_search.embedding <=> ${vector}`)
+          .orderBy(sql`smart_search.embedding <=> ${embedding}`)
           .limit(64),
       )
       .selectFrom('cte')
@@ -148,7 +147,7 @@ export class SearchRepository implements ISearchRepository {
     params: [
       {
         userIds: [DummyValue.UUID],
-        embedding: Array.from({ length: 512 }, Math.random),
+        embedding: DummyValue.VECTOR,
         numResults: 10,
         maxDistance: 0.6,
       },
@@ -159,7 +158,6 @@ export class SearchRepository implements ISearchRepository {
       throw new Error(`Invalid value for 'numResults': ${numResults}`);
     }
 
-    const vector = asVector(embedding);
     return this.db
       .with('cte', (qb) =>
         qb
@@ -167,14 +165,14 @@ export class SearchRepository implements ISearchRepository {
           .select([
             'asset_faces.id',
             'asset_faces.personId',
-            sql<number>`face_search.embedding <=> ${vector}`.as('distance'),
+            sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
           ])
           .innerJoin('assets', 'assets.id', 'asset_faces.assetId')
           .innerJoin('face_search', 'face_search.faceId', 'asset_faces.id')
           .where('assets.ownerId', '=', anyUuid(userIds))
           .where('assets.deletedAt', 'is', null)
           .$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null))
-          .orderBy(sql`face_search.embedding <=> ${vector}`)
+          .orderBy(sql`face_search.embedding <=> ${embedding}`)
           .limit(numResults),
       )
       .selectFrom('cte')
@@ -258,12 +256,11 @@ export class SearchRepository implements ISearchRepository {
       .execute() as any as Promise<AssetEntity[]>;
   }
 
-  async upsert(assetId: string, embedding: number[]): Promise<void> {
-    const vector = asVector(embedding);
+  async upsert(assetId: string, embedding: string): Promise<void> {
     await this.db
       .insertInto('smart_search')
-      .values({ assetId: asUuid(assetId), embedding: vector } as any)
-      .onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding: vector } as any))
+      .values({ assetId: asUuid(assetId), embedding } as any)
+      .onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding } as any))
       .execute();
   }
 
diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts
index 3fc838e5e9..611f8f69d3 100644
--- a/server/src/services/audit.service.ts
+++ b/server/src/services/audit.service.ts
@@ -201,21 +201,22 @@ export class AuditService extends BaseService {
       }
     }
 
-    const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.personRepository.getAll(pagination),
-    );
-    for await (const people of personPagination) {
-      for (const { id, thumbnailPath } of people) {
-        track(thumbnailPath);
-        const entity = { entityId: id, entityType: PathEntityType.PERSON };
-        if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
-          orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
-        }
+    let peopleCount = 0;
+    for await (const { id, thumbnailPath } of this.personRepository.getAll()) {
+      track(thumbnailPath);
+      const entity = { entityId: id, entityType: PathEntityType.PERSON };
+      if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
+        orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
       }
 
-      this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${people.length} people`);
+      if (peopleCount === JOBS_ASSET_PAGINATION_SIZE) {
+        this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`);
+        peopleCount = 0;
+      }
     }
 
+    this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`);
+
     const extras: string[] = [];
     for (const file of allFiles) {
       extras.push(file);
diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts
index f76f832cf3..1784428d31 100644
--- a/server/src/services/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -25,7 +25,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
 import { faceStub } from 'test/fixtures/face.stub';
 import { probeStub } from 'test/fixtures/media.stub';
 import { personStub } from 'test/fixtures/person.stub';
-import { newTestService } from 'test/utils';
+import { makeStream, newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 describe(MediaService.name, () => {
@@ -55,10 +55,8 @@ describe(MediaService.name, () => {
         items: [assetStub.image],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [personStub.newThumbnail],
-        hasNextPage: false,
-      });
+
+      personMock.getAll.mockReturnValue(makeStream([personStub.newThumbnail]));
       personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
 
       await sut.handleQueueGenerateThumbnails({ force: true });
@@ -72,7 +70,7 @@ describe(MediaService.name, () => {
         },
       ]);
 
-      expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
+      expect(personMock.getAll).toHaveBeenCalledWith(undefined);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.GENERATE_PERSON_THUMBNAIL,
@@ -86,10 +84,7 @@ describe(MediaService.name, () => {
         items: [assetStub.trashed],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream());
 
       await sut.handleQueueGenerateThumbnails({ force: true });
 
@@ -111,10 +106,7 @@ describe(MediaService.name, () => {
         items: [assetStub.archived],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream());
 
       await sut.handleQueueGenerateThumbnails({ force: true });
 
@@ -136,10 +128,7 @@ describe(MediaService.name, () => {
         items: [assetStub.image],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [personStub.noThumbnail, personStub.noThumbnail],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail]));
       personMock.getRandomFace.mockResolvedValueOnce(faceStub.face1);
 
       await sut.handleQueueGenerateThumbnails({ force: false });
@@ -147,7 +136,7 @@ describe(MediaService.name, () => {
       expect(assetMock.getAll).not.toHaveBeenCalled();
       expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
 
-      expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
+      expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
       expect(personMock.getRandomFace).toHaveBeenCalled();
       expect(personMock.update).toHaveBeenCalledTimes(1);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
@@ -165,11 +154,7 @@ describe(MediaService.name, () => {
         items: [assetStub.noResizePath],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
-
+      personMock.getAll.mockReturnValue(makeStream());
       await sut.handleQueueGenerateThumbnails({ force: false });
 
       expect(assetMock.getAll).not.toHaveBeenCalled();
@@ -181,7 +166,7 @@ describe(MediaService.name, () => {
         },
       ]);
 
-      expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
+      expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
     });
 
     it('should queue all assets with missing webp path', async () => {
@@ -189,11 +174,7 @@ describe(MediaService.name, () => {
         items: [assetStub.noWebpPath],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
-
+      personMock.getAll.mockReturnValue(makeStream());
       await sut.handleQueueGenerateThumbnails({ force: false });
 
       expect(assetMock.getAll).not.toHaveBeenCalled();
@@ -205,7 +186,7 @@ describe(MediaService.name, () => {
         },
       ]);
 
-      expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
+      expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
     });
 
     it('should queue all assets with missing thumbhash', async () => {
@@ -213,11 +194,7 @@ describe(MediaService.name, () => {
         items: [assetStub.noThumbhash],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
-
+      personMock.getAll.mockReturnValue(makeStream());
       await sut.handleQueueGenerateThumbnails({ force: false });
 
       expect(assetMock.getAll).not.toHaveBeenCalled();
@@ -229,7 +206,7 @@ describe(MediaService.name, () => {
         },
       ]);
 
-      expect(personMock.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { thumbnailPath: '' } });
+      expect(personMock.getAll).toHaveBeenCalledWith({ thumbnailPath: '' });
     });
   });
 
@@ -237,7 +214,7 @@ describe(MediaService.name, () => {
     it('should remove empty directories and queue jobs', async () => {
       assetMock.getAll.mockResolvedValue({ hasNextPage: false, items: [assetStub.image] });
       jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts);
-      personMock.getAll.mockResolvedValue({ hasNextPage: false, items: [personStub.withName] });
+      personMock.getAll.mockReturnValue(makeStream([personStub.withName]));
 
       await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.SUCCESS);
 
@@ -730,10 +707,7 @@ describe(MediaService.name, () => {
         items: [assetStub.video],
         hasNextPage: false,
       });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream());
 
       await sut.handleQueueVideoConversion({ force: true });
 
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index 7036bd32e8..2a5ee39dde 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -72,23 +72,20 @@ export class MediaService extends BaseService {
     }
 
     const jobs: JobItem[] = [];
-    const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.personRepository.getAll(pagination, { where: force ? undefined : { thumbnailPath: '' } }),
-    );
 
-    for await (const people of personPagination) {
-      for (const person of people) {
-        if (!person.faceAssetId) {
-          const face = await this.personRepository.getRandomFace(person.id);
-          if (!face) {
-            continue;
-          }
+    const people = this.personRepository.getAll(force ? undefined : { thumbnailPath: '' });
 
-          await this.personRepository.update({ id: person.id, faceAssetId: face.id });
+    for await (const person of people) {
+      if (!person.faceAssetId) {
+        const face = await this.personRepository.getRandomFace(person.id);
+        if (!face) {
+          continue;
         }
 
-        jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
+        await this.personRepository.update({ id: person.id, faceAssetId: face.id });
       }
+
+      jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
     }
 
     await this.jobRepository.queueAll(jobs);
@@ -114,16 +111,19 @@ export class MediaService extends BaseService {
       );
     }
 
-    const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.personRepository.getAll(pagination),
-    );
+    let jobs: { name: JobName.MIGRATE_PERSON; data: { id: string } }[] = [];
 
-    for await (const people of personPagination) {
-      await this.jobRepository.queueAll(
-        people.map((person) => ({ name: JobName.MIGRATE_PERSON, data: { id: person.id } })),
-      );
+    for await (const person of this.personRepository.getAll()) {
+      jobs.push({ name: JobName.MIGRATE_PERSON, data: { id: person.id } });
+
+      if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) {
+        await this.jobRepository.queueAll(jobs);
+        jobs = [];
+      }
     }
 
+    await this.jobRepository.queueAll(jobs);
+
     return JobStatus.SUCCESS;
   }
 
diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts
index a92433e88f..24d2b0e17f 100644
--- a/server/src/services/metadata.service.spec.ts
+++ b/server/src/services/metadata.service.spec.ts
@@ -1086,7 +1086,9 @@ describe(MetadataService.name, () => {
         ],
         [],
       );
-      expect(personMock.updateAll).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]);
+      expect(personMock.updateAll).toHaveBeenCalledWith([
+        { id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' },
+      ]);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.GENERATE_PERSON_THUMBNAIL,
diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts
index 15ea990235..406f80038c 100644
--- a/server/src/services/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -509,11 +509,11 @@ export class MetadataService extends BaseService {
       return;
     }
 
-    const facesToAdd: Partial<AssetFaceEntity>[] = [];
+    const facesToAdd: (Partial<AssetFaceEntity> & { assetId: string })[] = [];
     const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true });
     const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id]));
-    const missing: Partial<PersonEntity>[] = [];
-    const missingWithFaceAsset: Partial<PersonEntity>[] = [];
+    const missing: (Partial<PersonEntity> & { ownerId: string })[] = [];
+    const missingWithFaceAsset: { id: string; ownerId: string; faceAssetId: string }[] = [];
     for (const region of tags.RegionInfo.RegionList) {
       if (!region.Name) {
         continue;
@@ -540,7 +540,7 @@ export class MetadataService extends BaseService {
       facesToAdd.push(face);
       if (!existingNameMap.has(loweredName)) {
         missing.push({ id: personId, ownerId: asset.ownerId, name: region.Name });
-        missingWithFaceAsset.push({ id: personId, faceAssetId: face.id });
+        missingWithFaceAsset.push({ id: personId, ownerId: asset.ownerId, faceAssetId: face.id });
       }
     }
 
@@ -557,7 +557,7 @@ export class MetadataService extends BaseService {
     }
 
     if (facesToAdd.length > 0) {
-      this.logger.debug(`Creating ${facesToAdd} faces from metadata for asset ${asset.id}`);
+      this.logger.debug(`Creating ${facesToAdd.length} faces from metadata for asset ${asset.id}`);
     }
 
     if (facesToRemove.length > 0 || facesToAdd.length > 0) {
diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts
index 60cb370881..b18eb7dfd8 100644
--- a/server/src/services/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -20,8 +20,7 @@ import { faceStub } from 'test/fixtures/face.stub';
 import { personStub } from 'test/fixtures/person.stub';
 import { systemConfigStub } from 'test/fixtures/system-config.stub';
 import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
-import { newTestService } from 'test/utils';
-import { IsNull } from 'typeorm';
+import { makeStream, newTestService } from 'test/utils';
 import { Mocked } from 'vitest';
 
 const responseDto: PersonResponseDto = {
@@ -46,7 +45,7 @@ const face = {
   imageHeight: 500,
   imageWidth: 400,
 };
-const faceSearch = { faceId, embedding: [1, 2, 3, 4] };
+const faceSearch = { faceId, embedding: '[1, 2, 3, 4]' };
 const detectFaceMock: DetectedFaces = {
   faces: [
     {
@@ -495,14 +494,8 @@ describe(PersonService.name, () => {
     });
 
     it('should delete existing people and faces if forced', async () => {
-      personMock.getAll.mockResolvedValue({
-        items: [faceStub.face1.person, personStub.randomPerson],
-        hasNextPage: false,
-      });
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson]));
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       assetMock.getAll.mockResolvedValue({
         items: [assetStub.image],
         hasNextPage: false,
@@ -544,18 +537,12 @@ describe(PersonService.name, () => {
 
     it('should queue missing assets', async () => {
       jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       personMock.getAllWithoutFaces.mockResolvedValue([]);
 
       await sut.handleQueueRecognizeFaces({});
 
-      expect(personMock.getAllFaces).toHaveBeenCalledWith(
-        { skip: 0, take: 1000 },
-        { where: { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING } },
-      );
+      expect(personMock.getAllFaces).toHaveBeenCalledWith({ personId: null, sourceType: SourceType.MACHINE_LEARNING });
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.FACIAL_RECOGNITION,
@@ -569,19 +556,13 @@ describe(PersonService.name, () => {
 
     it('should queue all assets', async () => {
       jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream());
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       personMock.getAllWithoutFaces.mockResolvedValue([]);
 
       await sut.handleQueueRecognizeFaces({ force: true });
 
-      expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
+      expect(personMock.getAllFaces).toHaveBeenCalledWith(undefined);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.FACIAL_RECOGNITION,
@@ -595,26 +576,17 @@ describe(PersonService.name, () => {
 
     it('should run nightly if new face has been added since last run', async () => {
       personMock.getLatestFaceDate.mockResolvedValue(new Date().toISOString());
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
-      personMock.getAll.mockResolvedValue({
-        items: [],
-        hasNextPage: false,
-      });
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
+      personMock.getAll.mockReturnValue(makeStream());
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       personMock.getAllWithoutFaces.mockResolvedValue([]);
 
       await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
 
       expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
       expect(personMock.getLatestFaceDate).toHaveBeenCalledOnce();
-      expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
+      expect(personMock.getAllFaces).toHaveBeenCalledWith(undefined);
       expect(jobMock.queueAll).toHaveBeenCalledWith([
         {
           name: JobName.FACIAL_RECOGNITION,
@@ -631,10 +603,7 @@ describe(PersonService.name, () => {
 
       systemMock.get.mockResolvedValue({ lastRun: lastRun.toISOString() });
       personMock.getLatestFaceDate.mockResolvedValue(new Date(lastRun.getTime() - 1).toISOString());
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       personMock.getAllWithoutFaces.mockResolvedValue([]);
 
       await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
@@ -648,15 +617,8 @@ describe(PersonService.name, () => {
 
     it('should delete existing people if forced', async () => {
       jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
-      personMock.getAll.mockResolvedValue({
-        items: [faceStub.face1.person, personStub.randomPerson],
-        hasNextPage: false,
-      });
-      personMock.getAllFaces.mockResolvedValue({
-        items: [faceStub.face1],
-        hasNextPage: false,
-      });
-
+      personMock.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson]));
+      personMock.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
       personMock.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
 
       await sut.handleQueueRecognizeFaces({ force: true });
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index cc488a7f4e..45732c4e7c 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -50,7 +50,6 @@ import { ImmichFileResponse } from 'src/utils/file';
 import { mimeTypes } from 'src/utils/mime-types';
 import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
-import { IsNull } from 'typeorm';
 
 @Injectable()
 export class PersonService extends BaseService {
@@ -306,7 +305,7 @@ export class PersonService extends BaseService {
     );
     this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`);
 
-    const facesToAdd: (Partial<AssetFaceEntity> & { id: string })[] = [];
+    const facesToAdd: (Partial<AssetFaceEntity> & { id: string; assetId: string })[] = [];
     const embeddings: FaceSearchEntity[] = [];
     const mlFaceIds = new Set<string>();
     for (const face of asset.faces) {
@@ -414,18 +413,22 @@ export class PersonService extends BaseService {
     }
 
     const lastRun = new Date().toISOString();
-    const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
-      this.personRepository.getAllFaces(pagination, {
-        where: force ? undefined : { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING },
-      }),
+    const facePagination = this.personRepository.getAllFaces(
+      force ? undefined : { personId: null, sourceType: SourceType.MACHINE_LEARNING },
     );
 
-    for await (const page of facePagination) {
-      await this.jobRepository.queueAll(
-        page.map((face) => ({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id, deferred: false } })),
-      );
+    let jobs: { name: JobName.FACIAL_RECOGNITION; data: { id: string; deferred: false } }[] = [];
+    for await (const face of facePagination) {
+      jobs.push({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id, deferred: false } });
+
+      if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) {
+        await this.jobRepository.queueAll(jobs);
+        jobs = [];
+      }
     }
 
+    await this.jobRepository.queueAll(jobs);
+
     await this.systemMetadataRepository.set(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun });
 
     return JobStatus.SUCCESS;
@@ -441,7 +444,7 @@ export class PersonService extends BaseService {
     const face = await this.personRepository.getFaceByIdWithAssets(
       id,
       { person: true, asset: true, faceSearch: true },
-      { id: true, personId: true, sourceType: true, faceSearch: { embedding: true } },
+      { id: true, personId: true, sourceType: true, faceSearch: true },
     );
     if (!face || !face.asset) {
       this.logger.warn(`Face ${id} not found`);
diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts
index 0b0ee6b20f..d485f4244b 100644
--- a/server/src/services/smart-info.service.spec.ts
+++ b/server/src/services/smart-info.service.spec.ts
@@ -284,7 +284,7 @@ describe(SmartInfoService.name, () => {
     });
 
     it('should save the returned objects', async () => {
-      machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
+      machineLearningMock.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
 
       expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
 
@@ -293,7 +293,7 @@ describe(SmartInfoService.name, () => {
         '/uploads/user-id/thumbs/path.jpg',
         expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
       );
-      expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
+      expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
     });
 
     it('should skip invisible assets', async () => {
@@ -315,7 +315,7 @@ describe(SmartInfoService.name, () => {
     });
 
     it('should wait for database', async () => {
-      machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
+      machineLearningMock.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]');
       databaseMock.isBusy.mockReturnValue(true);
 
       expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
@@ -326,7 +326,7 @@ describe(SmartInfoService.name, () => {
         '/uploads/user-id/thumbs/path.jpg',
         expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
       );
-      expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
+      expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]');
     });
   });
 
diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts
index 4ccb68f2e0..7483ef6f92 100644
--- a/server/src/utils/database.ts
+++ b/server/src/utils/database.ts
@@ -42,7 +42,7 @@ export const asUuid = (id: string | Expression<string>) => sql<string>`${id}::uu
 
 export const anyUuid = (ids: string[]) => sql<string>`any(${`{${ids}}`}::uuid[])`;
 
-export const asVector = (embedding: number[]) => sql<number[]>`${`[${embedding}]`}::vector`;
+export const asVector = (embedding: number[]) => sql<string>`${`[${embedding}]`}::vector`;
 
 /**
  * Mainly for type debugging to make VS Code display a more useful tooltip.
diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts
index 45390cf92e..8f6c794790 100644
--- a/server/test/fixtures/asset.stub.ts
+++ b/server/test/fixtures/asset.stub.ts
@@ -824,7 +824,7 @@ export const assetStub = {
     duplicateId: null,
     smartSearch: {
       assetId: 'asset-id',
-      embedding: Array.from({ length: 512 }, Math.random),
+      embedding: '[1, 2, 3, 4]',
     },
     isOffline: false,
   }),
@@ -866,7 +866,7 @@ export const assetStub = {
     duplicateId: 'duplicate-id',
     smartSearch: {
       assetId: 'asset-id',
-      embedding: Array.from({ length: 512 }, Math.random),
+      embedding: '[1, 2, 3, 4]',
     },
     isOffline: false,
   }),
diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts
index b8c68d5bf4..4da4e6a0c4 100644
--- a/server/test/fixtures/face.stub.ts
+++ b/server/test/fixtures/face.stub.ts
@@ -19,7 +19,7 @@ export const faceStub = {
     imageHeight: 1024,
     imageWidth: 1024,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId1', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId1', embedding: '[1, 2, 3, 4]' },
   }),
   primaryFace1: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
     id: 'assetFaceId2',
@@ -34,7 +34,7 @@ export const faceStub = {
     imageHeight: 1024,
     imageWidth: 1024,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId2', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId2', embedding: '[1, 2, 3, 4]' },
   }),
   mergeFace1: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
     id: 'assetFaceId3',
@@ -49,7 +49,7 @@ export const faceStub = {
     imageHeight: 1024,
     imageWidth: 1024,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId3', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId3', embedding: '[1, 2, 3, 4]' },
   }),
   start: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
     id: 'assetFaceId5',
@@ -64,7 +64,7 @@ export const faceStub = {
     imageHeight: 2880,
     imageWidth: 2160,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId5', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId5', embedding: '[1, 2, 3, 4]' },
   }),
   middle: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
     id: 'assetFaceId6',
@@ -79,7 +79,7 @@ export const faceStub = {
     imageHeight: 500,
     imageWidth: 400,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId6', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId6', embedding: '[1, 2, 3, 4]' },
   }),
   end: Object.freeze<NonNullableProperty<AssetFaceEntity>>({
     id: 'assetFaceId7',
@@ -94,7 +94,7 @@ export const faceStub = {
     imageHeight: 500,
     imageWidth: 500,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId7', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId7', embedding: '[1, 2, 3, 4]' },
   }),
   noPerson1: Object.freeze<AssetFaceEntity>({
     id: 'assetFaceId8',
@@ -109,7 +109,7 @@ export const faceStub = {
     imageHeight: 1024,
     imageWidth: 1024,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId8', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId8', embedding: '[1, 2, 3, 4]' },
   }),
   noPerson2: Object.freeze<AssetFaceEntity>({
     id: 'assetFaceId9',
@@ -124,7 +124,7 @@ export const faceStub = {
     imageHeight: 1024,
     imageWidth: 1024,
     sourceType: SourceType.MACHINE_LEARNING,
-    faceSearch: { faceId: 'assetFaceId9', embedding: [1, 2, 3, 4] },
+    faceSearch: { faceId: 'assetFaceId9', embedding: '[1, 2, 3, 4]' },
   }),
   fromExif1: Object.freeze<AssetFaceEntity>({
     id: 'assetFaceId9',
diff --git a/server/test/utils.ts b/server/test/utils.ts
index 929fcb9da0..df8ca96c09 100644
--- a/server/test/utils.ts
+++ b/server/test/utils.ts
@@ -254,3 +254,10 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st
     }),
   } as unknown as ChildProcessWithoutNullStreams;
 });
+
+export async function* makeStream<T>(items: T[] = []): AsyncIterableIterator<T> {
+  for (const item of items) {
+    await Promise.resolve();
+    yield item;
+  }
+}