diff --git a/server/src/infra/migrations/1700713871511-UsePgVectors.ts b/server/src/infra/migrations/1700713871511-UsePgVectors.ts
new file mode 100644
index 00000000..9b13f836
--- /dev/null
+++ b/server/src/infra/migrations/1700713871511-UsePgVectors.ts
@@ -0,0 +1,53 @@
+import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
+import { MigrationInterface, QueryRunner } from 'typeorm';
+import { assertVectors } from '../database.config';
+
+export class UsePgVectors1700713871511 implements MigrationInterface {
+ name = 'UsePgVectors1700713871511';
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await assertVectors(queryRunner);
+
+ const faceDimQuery = await queryRunner.query(`
+ SELECT CARDINALITY(embedding::real[]) as dimsize
+ FROM asset_faces
+ LIMIT 1`);
+ const faceDimSize = faceDimQuery?.[0]?.['dimsize'] ?? 512;
+
+ const clipModelNameQuery = await queryRunner.query(`SELECT value FROM system_config WHERE key = 'machineLearning.clip.modelName'`);
+ const clipModelName: string = clipModelNameQuery?.[0]?.['value'] ?? 'ViT-B-32__openai';
+ const clipDimSize = getCLIPModelInfo(clipModelName.replace(/"/g, '')).dimSize;
+
+ await queryRunner.query(`
+ ALTER TABLE asset_faces
+ ALTER COLUMN embedding SET NOT NULL,
+ 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
+ AND CARDINALITY("clipEmbedding"::real[]) = ${clipDimSize}
+ AND array_position(si."clipEmbedding", NULL) IS NULL`);
+
+ await queryRunner.query(`ALTER TABLE smart_info DROP COLUMN IF EXISTS "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`);
+ }
+}
diff --git a/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
new file mode 100644
index 00000000..7a1a1144
--- /dev/null
+++ b/server/src/infra/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface {
+ name = 'AddCLIPEmbeddingIndex1700713994428';
+
+ 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 = $$
+ [indexing.hnsw]
+ m = 16
+ ef_construction = 300
+ $$);`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`DROP INDEX IF EXISTS clip_index`);
+ }
+}
diff --git a/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts
new file mode 100644
index 00000000..0ac7b0cd
--- /dev/null
+++ b/server/src/infra/migrations/1700714033632-AddFaceEmbeddingIndex.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface {
+ name = 'AddFaceEmbeddingIndex1700714033632';
+
+ 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 = $$
+ [indexing.hnsw]
+ m = 16
+ ef_construction = 300
+ $$);`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`DROP INDEX IF EXISTS face_index`);
+ }
+}
diff --git a/server/src/infra/migrations/1700714072055-AddSmartInfoTagsIndex.ts b/server/src/infra/migrations/1700714072055-AddSmartInfoTagsIndex.ts
new file mode 100644
index 00000000..b850d3da
--- /dev/null
+++ b/server/src/infra/migrations/1700714072055-AddSmartInfoTagsIndex.ts
@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddSmartInfoTagsIndex1700714072055 implements MigrationInterface {
+ name = 'AddSmartInfoTagsIndex1700714072055';
+
+ 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;`);
+ }
+}
diff --git a/server/src/infra/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts b/server/src/infra/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts
new file mode 100644
index 00000000..b42291f6
--- /dev/null
+++ b/server/src/infra/migrations/1700714140297-CreateSmartInfoTextSearchIndex.ts
@@ -0,0 +1,37 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class CreateSmartInfoTextSearchIndex1700714140297 implements MigrationInterface {
+ name = 'CreateSmartInfoTextSearchIndex1700714140297';
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ // https://dba.stackexchange.com/a/164081
+ await queryRunner.query(`
+ CREATE OR REPLACE FUNCTION f_concat_ws(text, text[])
+ RETURNS text
+ LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
+ 'SELECT array_to_string($2, $1)'`);
+
+ await queryRunner.query(`
+ ALTER TABLE smart_info ADD "smartInfoTextSearchableColumn" tsvector
+ GENERATED ALWAYS AS (
+ TO_TSVECTOR(
+ 'english',
+ f_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
+ USING GIN ("smartInfoTextSearchableColumn")`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`DROP FUNCTION IF EXISTS immutable_concat_ws`);
+ await queryRunner.query(`ALTER TABLE smart_info DROP IF EXISTS "smartInfoTextSearchableColumn"`);
+ }
+}
diff --git a/server/src/infra/migrations/1701665867595-AddExifCityIndex.ts b/server/src/infra/migrations/1701665867595-AddExifCityIndex.ts
new file mode 100644
index 00000000..9979762d
--- /dev/null
+++ b/server/src/infra/migrations/1701665867595-AddExifCityIndex.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddExifCityIndex1701665867595 implements MigrationInterface {
+ name = 'AddExifCityIndex1701665867595'
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`CREATE INDEX "exif_city" ON "exif" ("city") `);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`DROP INDEX "public"."exif_city"`);
+ }
+
+}
diff --git a/server/src/infra/migrations/1702084989965-AddWebSocketAttachmentTable.ts b/server/src/infra/migrations/1702084989965-AddWebSocketAttachmentTable.ts
new file mode 100644
index 00000000..c2fc0b22
--- /dev/null
+++ b/server/src/infra/migrations/1702084989965-AddWebSocketAttachmentTable.ts
@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddWebSocketAttachmentTable1702084989965 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(
+ 'CREATE TABLE IF NOT EXISTS "socket_io_attachments" (id bigserial UNIQUE, created_at timestamptz DEFAULT NOW(), payload bytea);',
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`DROP TABLE "socket_io_attachments"`);
+ }
+}
diff --git a/server/src/infra/migrations/1702257380990-DropNullIslandLatLong.ts b/server/src/infra/migrations/1702257380990-DropNullIslandLatLong.ts
new file mode 100644
index 00000000..173b2c59
--- /dev/null
+++ b/server/src/infra/migrations/1702257380990-DropNullIslandLatLong.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class DropNullIslandLatLong1702257380990 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(
+ 'UPDATE "exif" SET latitude = NULL, longitude = NULL WHERE latitude = 0 AND longitude = 0;',
+ );
+ }
+
+ public async down(): Promise<void> {
+ // There's no way to know which assets used to have 0/0 lat-long if we've
+ // already run this migration.
+ }
+}
diff --git a/server/Dockerfile b/server/Dockerfile
index b43058d5..3c2a0522 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -1,29 +1,37 @@
# dev build
-FROM ghcr.io/immich-app/base-server-dev:20231201@sha256:4701c0c5920c78e73040dd2b74d22042ffce393f1a9d3453d90a0ecf81ff8649 as dev
+FROM ghcr.io/immich-app/base-server-dev:20231214@sha256:cd5be516b27c0c402bee3a6a93d8c83dfd5a827c18a2343cb97b55f3be98151b as dev
+RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./
-RUN npm ci
+RUN npm ci && \
+ # sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
+ # they're marked as optional dependencies, so we need to copy them manually after pruning
+ rm -rf node_modules/@img/sharp-libvips* && \
+ rm -rf node_modules/@img/sharp-linuxmusl-x64
COPY server .
+ENV PATH="${PATH}:/usr/src/app/bin"
+ENTRYPOINT ["tini", "--", "/bin/sh"]
FROM dev AS prod
RUN npm run build
RUN npm prune --omit=dev --omit=optional
+COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
# web build
FROM node:iron-alpine3.18 as web
WORKDIR /usr/src/app
-COPY web/package.json web/package-lock.json ./
+COPY web/package*.json web/svelte.config.js .
RUN npm ci
COPY web .
RUN npm run build
# prod build
-FROM ghcr.io/immich-app/base-server-prod:20231201@sha256:b8e86cf4c3cad872f54bab25a83f7503480049eea5c0ae36a8b8460b13cad3b5
+FROM ghcr.io/immich-app/base-server-prod:20231214@sha256:b214f86683fde081b09beed2d7bfc28bec55c829751ccf2e02ad7dd18293f5e0
WORKDIR /usr/src/app
ENV NODE_ENV=production
diff --git a/docker/example.env b/docker/example.env
index 319bb39c..40d379f4 100644
--- a/docker/example.env
+++ b/docker/example.env
@@ -6,8 +6,7 @@ UPLOAD_LOCATION=./library
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION=release
-# Connection secrets for postgres and typesense. You should change these to random passwords
-TYPESENSE_API_KEY=some-random-text
+# Connection secret for postgres. You should change it to a random password
DB_PASSWORD=postgres
# The values below this line do not need to be changed
This PR bumps the version from v1.90.2 to v1.91.4. Please review the changes and merge this PR if everything looks good.
Upstream release notes
Monitored upstream files
-## Typesense
TYPESENSE_ENABLED
TYPESENSE_URL
TYPESENSE_HOST
typesense
TYPESENSE_PORT
8108
TYPESENSE_PROTOCOL
http
TYPESENSE_API_KEY
TYPESENSE_DATA_DIR
/data
- -:::info
-
TYPESENSE_URL
must start withha://
and then include abase64
encoded JSON string for the configuration.-
TYPESENSE_ENABLED
: Anything other thanfalse
, behaves astrue
. -Even undefined is treated astrue
.-- When
TYPESENSE_URL
is defined, the other typesense (TYPESENSE_*
) variables are ignored.-:::
-Typesense URL example JSON before encoding:
-```json -[
Machine Learning
| Variable | Description | Default | Services |
-V, --version output the version number -h, --help display help for command
Commands:
Checklist
ref #142