{"record":{"id":"8bb4e3838a846e70","repo":"immich-app/immich","slug":"invalid-value-for-numresults-numresults","errorCode":null,"errorMessage":"Invalid value for 'numResults': ${numResults}","messagePattern":"Invalid value for 'numResults': (.+?)","errorType":"exception","errorClass":"Error","httpStatus":500,"severity":"error","filePath":"server/src/repositories/search.repository.ts","lineNumber":355,"sourceCode":"    params: [DummyValue.UUID],\n  })\n  async getEmbedding(assetId: string) {\n    return this.db.selectFrom('smart_search').selectAll().where('assetId', '=', assetId).executeTakeFirst();\n  }\n\n  @GenerateSql({\n    params: [\n      {\n        userIds: [DummyValue.UUID],\n        embedding: DummyValue.VECTOR,\n        numResults: 10,\n        maxDistance: 0.6,\n      },\n    ],\n  })\n  searchFaces({ clusterGroupId, embedding, numResults, maxDistance, hasPerson, minBirthDate }: FaceEmbeddingSearch) {\n    if (!z.int().min(1).max(1000).safeParse(numResults).success) {\n      throw new Error(`Invalid value for 'numResults': ${numResults}`);\n    }\n\n    return this.db.transaction().execute(async (trx) => {\n      await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Face])}`.execute(trx);\n      return await trx\n        .with('cte', (qb) =>\n          qb\n            .selectFrom('asset_face')\n            .innerJoin('asset', 'asset.id', 'asset_face.assetId')\n            .innerJoin('face_search', 'face_search.faceId', 'asset_face.id')\n            .select([\n              'asset_face.id',\n              'asset_face.personGroupId',\n              sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),\n            ])\n            .where('asset.ownerId', 'in', (eb) =>\n              eb.selectFrom('user').select('user.id').where('user.clusterGroupId', '=', clusterGroupId),\n            )","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/immich-app/immich/blob/5666d57f15a66bd5518119c5d9f4d2b62f3a86c1/server/src/repositories/search.repository.ts#L337-L373","documentation":"This is a hand-rolled runtime guard inside Immich's SearchRepository.searchFaces, the vchordrq (pgvector) face-embedding similarity query. Before the SQL transaction runs, numResults (the LIMIT for the nearest-neighbor search) is validated with zod as z.int().min(1).max(1000); any non-integer, 0, negative, NaN, undefined, or value above 1000 throws this plain Error, aborting the facial-recognition job. The guard exists because numResults is passed straight into .limit() on an expensive vector index scan, so out-of-range values would produce degenerate or runaway queries.","triggerScenarios":"The main production caller is PersonService.handleRecognizeFaces (server/src/services/person.service.ts:501), which passes machineLearning.facialRecognition.minFaces directly as numResults. The config DTO only enforces z.int().min(1) with NO upper bound (server/src/dtos/config.dto.ts:243-247), so setting Facial Recognition > Minimum faces to a value above 1000 in the admin UI makes every FacialRecognition job queue item throw this error. It is also thrown when any direct caller of searchFaces passes 0, a float like 10.5, a numeric string, NaN, or an unbounded computed value (e.g. array.length when the array is empty).","commonSituations":"Admins raising the 'minimum faces for recognition' threshold very high to suppress noisy person clusters, then facial recognition silently failing for all jobs. Custom forks or scripts calling searchRepository.searchFaces with a dynamically derived count. Type drift after refactors where numResults becomes a string or optional field (undefined fails the zod parse).","solutions":["Set Machine Learning > Facial Recognition > Minimum faces back to an integer between 1 and 1000 in Administration > Settings, then re-run the Facial Recognition job — this fixes the common config-driven case.","If you call searchFaces directly, pass an integer clamped to 1..1000: Math.min(1000, Math.max(1, Math.trunc(numResults))).","Harden the config DTO so invalid values are rejected at save time: change minFaces in src/dtos/config.dto.ts to z.int().min(1).max(1000) to match the repository contract.","Check the facial-recognition queue logs to confirm which value was actually received — the message interpolates the offending numResults value."],"exampleFix":"// before (src/services/person.service.ts / admin config)\n// admin sets facialRecognition.minFaces = 5000; DTO allows it (no max)\nnumResults: machineLearning.facialRecognition.minFaces, // throws: Invalid value for 'numResults': 5000\n\n// after — enforce the repository contract at the config boundary (src/dtos/config.dto.ts)\nminFaces: z\n  .int()\n  .min(1)\n  .max(1000) // match searchFaces' z.int().min(1).max(1000)\n  .describe('Minimum number of faces required for recognition')","handlingStrategy":"validation","validationCode":"// Before calling searchFaces (or queueing facial recognition with a custom minFaces):\nimport { z } from 'zod';\n\nconst NumResults = z.int().min(1).max(1000);\nconst parsed = NumResults.safeParse(numResults);\nif (!parsed.success) {\n  // clamp instead of aborting a long-running recognition job\n  numResults = Math.min(1000, Math.max(1, Math.trunc(Number(numResults) || 1)));\n}","typeGuard":"import { z } from 'zod';\n\nconst isValidNumResults = (value: unknown): value is number =>\n  z.int().min(1).max(1000).safeParse(value).success;","tryCatchPattern":"// Inside a job handler / wrapper around searchFaces:\ntry {\n  return await this.searchRepository.searchFaces({ ... });\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith(\"Invalid value for 'numResults'\")) {\n    this.logger.warn(`Skipping face ${id}: ${error.message} (check facialRecognition.minFaces config)`);\n    return JobStatus.Failed; // do not retry an unchanged config value\n  }\n  throw error;\n}","preventionTips":["Keep machineLearning.facialRecognition.minFaces within 1..1000 — mirror the repository's .max(1000) in the config DTO so bad values are rejected at save time.","Never derive numResults from unbounded inputs (array lengths, user counts) without clamping.","Unit-test boundary values 0, 1, 1000, 1001 and non-integers against the searchFaces contract."],"tags":["immich","facial-recognition","vector-search","zod","validation","postgres","job-failure"],"backgroundTag":"parameter-validation-failed","analyzedSha":"5666d57f15a66bd5518119c5d9f4d2b62f3a86c1","analyzedAt":"2026-08-21T18:08:19.313Z","contentChangedAt":"2026-08-21T18:08:19.313Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}