mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	chore(server): startup check for pgvecto.rs (#5815)
* startup check for pgvecto.rs * prefilter after assertion * formatting * add assert to migration * more specific import * use runner
This commit is contained in:
		
							parent
							
								
									fade8b627f
								
							
						
					
					
						commit
						de1514a441
					
				@ -1,5 +1,5 @@
 | 
			
		||||
import { envName, isDev, serverVersion } from '@app/domain';
 | 
			
		||||
import { WebSocketAdapter, enablePrefilter } from '@app/infra';
 | 
			
		||||
import { WebSocketAdapter, databaseChecks } from '@app/infra';
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { NestFactory } from '@nestjs/core';
 | 
			
		||||
import { NestExpressApplication } from '@nestjs/platform-express';
 | 
			
		||||
@ -31,7 +31,7 @@ export async function bootstrap() {
 | 
			
		||||
  app.useStaticAssets('www');
 | 
			
		||||
  app.use(app.get(AppService).ssr(excludePaths));
 | 
			
		||||
 | 
			
		||||
  await enablePrefilter();
 | 
			
		||||
  await databaseChecks();
 | 
			
		||||
 | 
			
		||||
  const server = await app.listen(port);
 | 
			
		||||
  server.requestTimeout = 30 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
import { DataSource, QueryRunner } from 'typeorm';
 | 
			
		||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
 | 
			
		||||
 | 
			
		||||
const url = process.env.DB_URL;
 | 
			
		||||
@ -26,9 +26,50 @@ export const databaseConfig: PostgresConnectionOptions = {
 | 
			
		||||
// this export is used by TypeORM commands in package.json#scripts
 | 
			
		||||
export const dataSource = new DataSource(databaseConfig);
 | 
			
		||||
 | 
			
		||||
export async function enablePrefilter() {
 | 
			
		||||
export async function databaseChecks() {
 | 
			
		||||
  if (!dataSource.isInitialized) {
 | 
			
		||||
    await dataSource.initialize();
 | 
			
		||||
  }
 | 
			
		||||
  await dataSource.query(`SET vectors.enable_prefilter = on`);
 | 
			
		||||
 | 
			
		||||
  await assertVectors(dataSource);
 | 
			
		||||
  await enablePrefilter(dataSource);
 | 
			
		||||
  await dataSource.runMigrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function enablePrefilter(runner: DataSource | QueryRunner) {
 | 
			
		||||
  await runner.query(`SET vectors.enable_prefilter = on`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getExtensionVersion(extName: string, runner: DataSource | QueryRunner): Promise<string | null> {
 | 
			
		||||
  const res = await runner.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extName]);
 | 
			
		||||
  return res[0]?.['extversion'] ?? null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getPostgresVersion(runner: DataSource | QueryRunner): Promise<string> {
 | 
			
		||||
  const res = await runner.query(`SHOW server_version`);
 | 
			
		||||
  return res[0]['server_version'].split('.')[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function assertVectors(runner: DataSource | QueryRunner) {
 | 
			
		||||
  const postgresVersion = await getPostgresVersion(runner);
 | 
			
		||||
  const expected = ['0.1.1', '0.1.11'];
 | 
			
		||||
  const image = `tensorchord/pgvecto-rs:pg${postgresVersion}-v${expected[expected.length - 1]}`;
 | 
			
		||||
 | 
			
		||||
  await runner.query('CREATE EXTENSION IF NOT EXISTS vectors').catch((err) => {
 | 
			
		||||
    console.error(
 | 
			
		||||
      'Failed to create pgvecto.rs extension. ' +
 | 
			
		||||
        `If you have not updated your Postgres instance to an image that supports pgvecto.rs (such as '${image}'), please do so. ` +
 | 
			
		||||
        'See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0',
 | 
			
		||||
    );
 | 
			
		||||
    throw err;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const version = await getExtensionVersion('vectors', runner);
 | 
			
		||||
  if (version != null && !expected.includes(version)) {
 | 
			
		||||
    throw new Error(
 | 
			
		||||
      `The pgvecto.rs extension version is ${version} instead of the expected version ${
 | 
			
		||||
        expected[expected.length - 1]
 | 
			
		||||
      }.` + `If you're using the 'latest' tag, please switch to '${image}'.`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,13 @@
 | 
			
		||||
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
 | 
			
		||||
@ -15,8 +18,6 @@ export class UsePgVectors1700713871511 implements MigrationInterface {
 | 
			
		||||
    const clipModelName: string = clipModelNameQuery?.[0]?.['value'] ?? 'ViT-B-32__openai';
 | 
			
		||||
    const clipDimSize = getCLIPModelInfo(clipModelName.replace(/"/g, '')).dimSize;
 | 
			
		||||
 | 
			
		||||
    await queryRunner.query('CREATE EXTENSION IF NOT EXISTS vectors');
 | 
			
		||||
 | 
			
		||||
    await queryRunner.query(`
 | 
			
		||||
        ALTER TABLE asset_faces 
 | 
			
		||||
        ALTER COLUMN embedding SET NOT NULL,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { envName, serverVersion } from '@app/domain';
 | 
			
		||||
import { WebSocketAdapter, enablePrefilter } from '@app/infra';
 | 
			
		||||
import { WebSocketAdapter, databaseChecks } from '@app/infra';
 | 
			
		||||
import { ImmichLogger } from '@app/infra/logger';
 | 
			
		||||
import { NestFactory } from '@nestjs/core';
 | 
			
		||||
import { MicroservicesModule } from './microservices.module';
 | 
			
		||||
@ -12,7 +12,7 @@ export async function bootstrap() {
 | 
			
		||||
 | 
			
		||||
  app.useLogger(app.get(ImmichLogger));
 | 
			
		||||
  app.useWebSocketAdapter(new WebSocketAdapter(app));
 | 
			
		||||
  await enablePrefilter();
 | 
			
		||||
  await databaseChecks();
 | 
			
		||||
 | 
			
		||||
  await app.listen(port);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { AssetCreate, IJobRepository, JobItem, JobItemHandler, LibraryResponseDto, QueueName } from '@app/domain';
 | 
			
		||||
import { AppModule } from '@app/immich';
 | 
			
		||||
import { dataSource } from '@app/infra';
 | 
			
		||||
import { dataSource, databaseChecks } from '@app/infra';
 | 
			
		||||
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
 | 
			
		||||
import { INestApplication } from '@nestjs/common';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
@ -22,11 +22,7 @@ export interface ResetOptions {
 | 
			
		||||
}
 | 
			
		||||
export const db = {
 | 
			
		||||
  reset: async (options?: ResetOptions) => {
 | 
			
		||||
    if (!dataSource.isInitialized) {
 | 
			
		||||
      await dataSource.initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await dataSource.query(`SET vectors.enable_prefilter = on`);
 | 
			
		||||
    await databaseChecks();
 | 
			
		||||
    await dataSource.transaction(async (em) => {
 | 
			
		||||
      const entities = options?.entities || [];
 | 
			
		||||
      const tableNames =
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user