use pgvecto.rs

This commit is contained in:
mertalev 2023-11-11 18:59:46 -05:00
parent f78d70f87a
commit 71388eaec2
No known key found for this signature in database
GPG key ID: 9181CD92C0A1C5E3
27 changed files with 586 additions and 165 deletions

View file

@ -99,8 +99,7 @@ services:
database:
container_name: immich_postgres
# image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
image: ankane/pgvector
image: tensorchord/pgvecto-rs
env_file:
- .env
environment:

View file

@ -57,8 +57,7 @@ services:
database:
container_name: immich_postgres
# image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
image: ankane/pgvector
image: tensorchord/pgvecto-rs
env_file:
- .env
environment:

View file

@ -60,8 +60,7 @@ services:
database:
container_name: immich_postgres
# image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
image: ankane/pgvector
image: tensorchord/pgvecto-rs
env_file:
- .env
environment:

View file

@ -54,6 +54,7 @@
"ua-parser-js": "^1.0.35"
},
"devDependencies": {
"@esbuild/linux-x64": "^0.19.5",
"@nestjs/cli": "^10.1.16",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.2",
@ -914,6 +915,21 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz",
"integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==",
"cpu": [
"x64"
],
"dev": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -13390,6 +13406,12 @@
}
}
},
"@esbuild/linux-x64": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz",
"integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==",
"dev": true
},
"@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",

View file

@ -81,6 +81,7 @@
"ua-parser-js": "^1.0.35"
},
"devDependencies": {
"@esbuild/linux-x64": "^0.19.5",
"@nestjs/cli": "^10.1.16",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.2",

View file

@ -25,9 +25,11 @@ export interface PersonStatistics {
assets: number;
}
export type Embedding = number[];
export interface EmbeddingSearch {
ownerId: string;
embedding: number[];
embedding: Embedding;
numResults: number;
maxDistance?: number;
}

View file

@ -1,9 +1,9 @@
import { AssetEntity, SmartInfoEntity } from '@app/infra/entities';
import { EmbeddingSearch } from '../repositories';
import { Embedding, EmbeddingSearch } from '../repositories';
export const ISmartInfoRepository = 'ISmartInfoRepository';
export interface ISmartInfoRepository {
searchByEmbedding(search: EmbeddingSearch): Promise<AssetEntity[]>;
upsert(info: Partial<SmartInfoEntity>): Promise<void>;
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
}

View file

@ -105,7 +105,7 @@ export class SmartInfoService {
machineLearning.clip,
);
await this.repository.upsert({ assetId: asset.id, clipEmbedding: clipEmbedding });
await this.repository.upsert({ assetId: asset.id }, clipEmbedding);
return true;
}

View file

@ -22,6 +22,7 @@ import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity';
import { TagEntity } from './tag.entity';
import { UserEntity } from './user.entity';
import { SmartSearchEntity } from '.';
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
@ -137,6 +138,9 @@ export class AssetEntity {
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
smartInfo?: SmartInfoEntity;
@OneToOne(() => SmartSearchEntity, (smartSearchEntity) => smartSearchEntity.asset)
smartSearch?: SmartSearchEntity;
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
@JoinTable({ name: 'tag_asset' })
tags!: TagEntity[];

View file

@ -15,6 +15,7 @@ import { PartnerEntity } from './partner.entity';
import { PersonEntity } from './person.entity';
import { SharedLinkEntity } from './shared-link.entity';
import { SmartInfoEntity } from './smart-info.entity';
import { SmartSearchEntity } from './smart-search.entity';
import { SystemConfigEntity } from './system-config.entity';
import { SystemMetadataEntity } from './system-metadata.entity';
import { TagEntity } from './tag.entity';
@ -38,6 +39,7 @@ export * from './partner.entity';
export * from './person.entity';
export * from './shared-link.entity';
export * from './smart-info.entity';
export * from './smart-search.entity';
export * from './system-config.entity';
export * from './system-metadata.entity';
export * from './tag.entity';
@ -61,6 +63,7 @@ export const databaseEntities = [
PersonEntity,
SharedLinkEntity,
SmartInfoEntity,
SmartSearchEntity,
SystemConfigEntity,
SystemMetadataEntity,
TagEntity,

View file

@ -15,11 +15,4 @@ export class SmartInfoEntity {
@Column({ type: 'text', array: true, nullable: true })
objects!: string[] | null;
@Column({
type: 'float4',
array: true,
nullable: true,
})
clipEmbedding!: number[] | null;
}

View file

@ -0,0 +1,18 @@
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
import { AssetEntity } from './asset.entity';
@Entity('smart_search')
export class SmartSearchEntity {
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset?: AssetEntity;
@PrimaryColumn()
assetId!: string;
@Column({
type: 'float4',
array: true,
})
embedding!: number[];
}

View file

@ -1,32 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UsePgVector1693228677355 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE EXTENSION IF NOT EXISTS vector');
const faceDimQuery = await queryRunner.query(`
SELECT CARDINALITY("embedding")
FROM "asset_faces"
LIMIT 1`);
const clipDimQuery = await queryRunner.query(`
SELECT CARDINALITY("clipEmbedding")
FROM "smart_info"
LIMIT 1`);
const faceDimSize = faceDimQuery[0] ?? 512;
const clipDimSize = clipDimQuery[0] ?? 512;
await queryRunner.query(`
ALTER TABLE "asset_faces"
ALTER COLUMN "embedding" TYPE vector(${faceDimSize})`);
await queryRunner.query(
`ALTER TABLE "smart_info"
ALTER COLUMN "clipEmbedding" TYPE vector(${clipDimSize})`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "embedding" TYPE real array`);
await queryRunner.query(`ALTER TABLE "smart_info" ALTER COLUMN "clipEmbedding" TYPE real array`);
}
}

View file

@ -1,30 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSmartInfoTextSearchColumn1696875736010 implements MigrationInterface {
name = 'AddSmartInfoTextSearchColumn1696875736010';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION immutable_concat_ws(text, text[])
RETURNS text
LANGUAGE internal IMMUTABLE PARALLEL SAFE AS
'text_concat_ws'`);
await queryRunner.query(`
ALTER TABLE "smart_info" ADD "smartInfoTextSearchableColumn" tsvector
GENERATED ALWAYS AS (
TO_TSVECTOR(
'english',
immutable_concat_ws(
' '::text,
COALESCE(tags, array[]::text[]) || COALESCE(objects, array[]::text[])
)
)
)
STORED NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP FUNCTION IF EXISTS immutable_concat_ws(text, text[])`);
await queryRunner.query(`ALTER TABLE "smart_info" DROP COLUMN IF EXISTS "smartInfoTextSearchableColumn"`);
}
}

View file

@ -4,6 +4,25 @@ export class CreateSmartInfoTextSearchIndex1696876192604 implements MigrationInt
name = 'CreateSmartInfoTextSearchIndex1696876192604';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION immutable_concat_ws(text, text[])
RETURNS text
LANGUAGE internal IMMUTABLE PARALLEL SAFE AS
'text_concat_ws'`);
await queryRunner.query(`
ALTER TABLE smart_info ADD "smartInfoTextSearchableColumn" tsvector
GENERATED ALWAYS AS (
TO_TSVECTOR(
'english',
immutable_concat_ws(
' '::text,
COALESCE(tags, array[]::text[]) || COALESCE(objects, array[]::text[])
)
)
)
STORED NOT NULL`);
await queryRunner.query(`
CREATE INDEX smart_info_text_searchable_idx
ON smart_info
@ -11,6 +30,7 @@ export class CreateSmartInfoTextSearchIndex1696876192604 implements MigrationInt
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS smart_info_text_searchable_idx ON smart_info`);
await queryRunner.query(`DROP FUNCTION IF EXISTS immutable_concat_ws`);
await queryRunner.query(`ALTER TABLE smart_info DROP IF EXISTS "smartInfoTextSearchableColumn"`);
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddExifCityIndex1699419684990 implements MigrationInterface {
name = 'AddExifCityIndex1699419684990'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS exif_city ON exif (city);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS exif_city;`);
}
}

View file

@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddSmartInfoTagsIndex1699419700539 implements MigrationInterface {
name = 'AddSmartInfoTagsIndex1699419700539'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS si_tags ON smart_info USING GIN (tags);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS si_tags;`);
}
}

View file

@ -1,19 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCLIPEmbeddingIndex1699586761207 implements MigrationInterface {
name = 'AddCLIPEmbeddingIndex1699586761207';
public async up(queryRunner: QueryRunner): Promise<void> {
console.log('Creating CLIP index. This may take a while...');
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS clip_index
ON smart_info
USING hnsw ("clipEmbedding" vector_ip_ops)
WITH (m = 16, ef_construction = 128)`);
await queryRunner.query(`SET hnsw.ef_search = 250`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS clip_index`);
}
}

View file

@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UsePgVector1699746198141 implements MigrationInterface {
name = 'UsePgVector1699746198141';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP EXTENSION IF EXISTS vectors');
await queryRunner.query('CREATE EXTENSION vectors');
const faceDimQuery = await queryRunner.query(`
SELECT CARDINALITY(embedding) as dimsize
FROM asset_faces
LIMIT 1`);
const clipDimQuery = await queryRunner.query(`
SELECT CARDINALITY("clipEmbedding") as dimsize
FROM smart_info
LIMIT 1`);
const faceDimSize = faceDimQuery?.[0]?.['dimsize'] ?? 512;
const clipDimSize = clipDimQuery?.[0]?.['dimsize'] ?? 512;
await queryRunner.query(`ALTER TABLE asset_faces ALTER COLUMN embedding TYPE vector(${faceDimSize})`);
await queryRunner.query(`CREATE TABLE smart_search (
"assetId" uuid PRIMARY KEY NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
embedding vector(${clipDimSize}) NOT NULL )`);
await queryRunner.query(`
INSERT INTO smart_search("assetId", embedding)
SELECT si."assetId", si."clipEmbedding"
FROM smart_info si
WHERE "clipEmbedding" IS NOT NULL
`);
await queryRunner.query(`ALTER TABLE smart_info DROP COLUMN "clipEmbedding"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE asset_faces ALTER COLUMN embedding TYPE real array`);
await queryRunner.query(`ALTER TABLE smart_info ADD COLUMN IF NOT EXISTS "clipEmbedding" TYPE real array`);
await queryRunner.query(`
INSERT INTO smart_info
("assetId", "clipEmbedding")
SELECT s."assetId", s.embedding
FROM smart_search s
ON CONFLICT (s."assetId") DO UPDATE SET "clipEmbedding" = s.embedding
`);
await queryRunner.query(`DROP TABLE IF EXISTS smart_search`);
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCLIPEmbeddingIndex1699746301742 implements MigrationInterface {
name = 'AddCLIPEmbeddingIndex1699746301742';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS clip_index ON smart_search
USING vectors (embedding cosine_ops) WITH (options = $$
capacity = 2097152
[indexing.hnsw]
m = 16
ef_construction = 300
$$);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS clip_index`);
}
}

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddExifCityIndex1699746316571 implements MigrationInterface {
name = 'AddExifCityIndex1699746316571';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS exif_city ON exif (city);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS exif_city;`);
}
}

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSmartInfoTagsIndex1699746328479 implements MigrationInterface {
name = 'AddSmartInfoTagsIndex1699746328479';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS si_tags ON smart_info USING GIN (tags);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS si_tags;`);
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddFaceEmbeddingIndex1699746444644 implements MigrationInterface {
name = 'AddFaceEmbeddingIndex1699746444644';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS face_index ON asset_faces
USING vectors (embedding cosine_ops) WITH (options = $$
capacity = 2097152
[indexing.hnsw]
m = 16
ef_construction = 300
$$);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS face_index`);
}
}

View file

@ -477,13 +477,13 @@ export class AssetRepository implements IAssetRepository {
case WithoutProperty.CLIP_ENCODING:
relations = {
smartInfo: true,
smartSearch: true,
};
where = {
isVisible: true,
resizePath: Not(IsNull()),
smartInfo: {
clipEmbedding: IsNull(),
smartSearch: {
embedding: IsNull(),
},
};
break;

View file

@ -1,9 +1,9 @@
import { EmbeddingSearch, ISmartInfoRepository } from '@app/domain';
import { Embedding, EmbeddingSearch, ISmartInfoRepository } from '@app/domain';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import AsyncLock from 'async-lock';
import { Repository } from 'typeorm';
import { AssetEntity, SmartInfoEntity } from '../entities';
import { AssetEntity, SmartInfoEntity, SmartSearchEntity } from '../entities';
import { asVector } from '../infra.utils';
@Injectable()
@ -12,70 +12,72 @@ export class SmartInfoRepository implements ISmartInfoRepository {
private lock: AsyncLock;
private curDimSize: number | undefined;
constructor(@InjectRepository(SmartInfoEntity) private repository: Repository<SmartInfoEntity>) {
constructor(
@InjectRepository(SmartInfoEntity) private repository: Repository<SmartInfoEntity>,
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository<SmartSearchEntity>) {
this.lock = new AsyncLock();
}
async searchByEmbedding({ ownerId, embedding, numResults }: EmbeddingSearch): Promise<AssetEntity[]> {
const results = await this.repository
.createQueryBuilder('smartInfo')
.useTransaction(true)
.leftJoinAndSelect('smartInfo.asset', 'asset')
.where('asset.ownerId = :ownerId', { ownerId })
.orderBy(`smartInfo.clipEmbedding <=> :embedding`)
.setParameters({ embedding: asVector(embedding) })
.limit(numResults)
.getMany();
let results: AssetEntity[] = await this.assetRepository.createQueryBuilder('a')
.innerJoin('a.smartSearch', 's')
.where('a.ownerId = :ownerId')
.leftJoinAndSelect('a.exifInfo', 'e')
.orderBy('s.embedding <=> :embedding')
.setParameters({ embedding: asVector(embedding), ownerId })
.limit(numResults)
.getMany();
return results.map((result) => result.asset).filter((asset): asset is AssetEntity => !!asset);
return results;
}
async upsert(info: Partial<SmartInfoEntity>): Promise<void> {
const { clipEmbedding, ...withoutEmbedding } = info;
await this.repository.upsert(withoutEmbedding, { conflictPaths: ['assetId'] });
if (!clipEmbedding || !info.assetId) return;
async upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void> {
await this.repository.upsert(smartInfo, { conflictPaths: ['assetId'] });
if (!smartInfo.assetId || !embedding) return;
try {
await this.updateEmbedding(clipEmbedding, info.assetId);
await this.upsertEmbedding(smartInfo.assetId, embedding);
} catch (e) {
await this.updateDimSize(clipEmbedding.length);
await this.updateEmbedding(clipEmbedding, info.assetId);
await this.updateDimSize(embedding.length);
await this.upsertEmbedding(smartInfo.assetId, embedding);
}
}
private async updateEmbedding(embedding: number[], assetId: string): Promise<void> {
await this.repository.manager.query(`UPDATE "smart_info" SET "clipEmbedding" = $1 WHERE "assetId" = $2`, [
asVector(embedding),
assetId,
]);
private async upsertEmbedding(assetId: string, embedding: number[]): Promise<void> {
await this.smartSearchRepository.manager.query(
`INSERT INTO smart_search ($1, $2) ON CONFLICT ("assetId") SET embedding = $2`,
[assetId, asVector(embedding)],
);
}
/*
* note: never use this with user input
* this does not parameterize the query because it is not possible to parameterize the column type
*/
*/
private async updateDimSize(dimSize: number): Promise<void> {
await this.lock.acquire('updateDimSizeLock', async () => {
if (this.curDimSize === dimSize) return;
this.logger.log(`Updating CLIP dimension size to ${dimSize}`);
await this.repository.manager.query(`
await this.smartSearchRepository.manager.query(`
BEGIN;
ALTER TABLE smart_info
DROP COLUMN "clipEmbedding",
ADD COLUMN "clipEmbedding" vector(${dimSize});
ALTER TABLE smart_search
DROP COLUMN embedding,
ADD COLUMN embedding vector(${dimSize});
CREATE INDEX IF NOT EXISTS clip_index
ON smart_info
USING hnsw ("clipEmbedding" vector_ip_ops)
WITH (m = 16, ef_construction = 128);
CREATE INDEX clip_index ON smart_search
USING vectors (embedding dot_ops) WITH (options = $$
capacity = 2097152
[indexing.hnsw]
m = 16
ef_construction = 300
$$);
SET hnsw.ef_search = 250;
COMMIT;
`
);
`);
this.curDimSize = dimSize;
this.logger.log(`Successfully updated CLIP dimension size to ${dimSize}`);

352
web/package-lock.json generated
View file

@ -29,6 +29,7 @@
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.22.5",
"@esbuild/linux-x64": "^0.19.5",
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
"@sveltejs/adapter-static": "^2.0.3",
@ -1882,6 +1883,54 @@
"gl-matrix": "^3.4.3"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
@ -1898,6 +1947,293 @@
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz",
"integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==",
"cpu": [
"x64"
],
"dev": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -5413,6 +5749,22 @@
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/esbuild/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",

View file

@ -22,6 +22,7 @@
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.22.5",
"@esbuild/linux-x64": "^0.19.5",
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
"@sveltejs/adapter-static": "^2.0.3",