Face Comparison: Querying different_face Results from the vuer_oss DB
Summary
A customer (“P”) asked whether failed / different_face face-comparison results are queryable in the vuer_oss Postgres DB, and how to extract them — portrait step plus liveness step — for a given date range without a new release. This triage verified the answer against the local codebase (/Users/levander/coding/facekom/vuer_oss, branch feature/FKITDEV-8747).
Core Conclusion
The customer’s premise — “the difference values are not in the DB” — is false on the write side. Face comparison results, including failing ones, are persisted to the
faceComparisonstable. This is a read-side query + threshold-interpretation problem, solvable today with one read-only SQL query. No release is needed.
Severity P3 — informational / data-extraction request, no production defect.
Why the Customer Thinks Failures Are Missing
Two things make failures look absent, neither of which means the data isn’t stored:
different_faceis not a stored status. It is a verdict computed at read time. ThefaceComparisonstable only storesstatus ∈ {created, failed, success}plus a numericeuclideanDistance. A SQL query keyed off a stored “success” flag will silently miss every failing-but-status:'success'comparison (a successfully computed comparison whose distance is simply too large).- A
status:'success'filter exists in exactly one place —server/web/routes/room.endpoint.js:145, the operator room view. If the customer’s prior working SQL was lifted from that endpoint’s behaviour, it filters out nothing relevant here, but it primed the expectation that “failed” rows are a distinct stored class. They are not.
Terminology —
different_faceis derived, not stored
different_face≈CHECK_FAILUREfromSelfServiceCheckerService.getFaceComparisonResult()(server/service/SelfServiceCheckerService.js:132-153): returned wheneuclideanDistanceexceeds all configured threshold levels. To find these via SQL you must apply the threshold yourself — see the MVP query.
What Is Actually Stored
FaceComparison model
- Model:
server/db/model/faceComparison.js:18-37→ Postgres tablefaceComparisons. - Columns:
idstatus— STRING enum['created', 'failed', 'success'], default'created'euclideanDistance— FLOAT, nullable. The column comment at line 31 states it now holds cosine distance (range 0–2) despite the legacy name.createdAt,updatedAt
- FK columns (
server/db/models.js:298-309):recognitionFromId,recognitionToId(→faceRecognitions),roomId,selfServiceRoomId,customerId,userId. - No
flowId/taskId/ step column — you cannot directly tell from the row whether it came from the portrait step or the liveness step. withRecognitionsscope atmodels.js:300-305. Emits afaceComparison:updatedServiceBus event onafterSave.- Created by
FaceRecognitionService.createFaceComparisonModel()(server/service/FaceRecognitionService.js:140-164) viacompareFaceDetections()(:213-223).euclideanDistanceis stored unconditionally, regardless of any threshold (:152-161) — this is the crux of why failing comparisons are queryable.
FaceRecognition model
- Model:
server/db/model/faceRecognition.js:18-30— a per-image detection record. - Failed detection rows are also persisted.
RecognitionService.js:234and:362setstatus = result.isResultSuccess() ? 'success' : 'failed', then callcreateFaceRecognitionModelunconditionally. - Carries
imageCategory, which is the only (heuristic) lever for distinguishing portrait vs liveness — see caveats.
Face comparison persistence flow
flowchart TD Step["Flow step triggers<br/>face comparison"] --> Sites{"Which call site?"} Sites -->|"Liveness V2<br/>SelfServiceV2Service.js:1390"| Gate{"task.options<br/>.recognitionOptions<br/>.compareFaceWith set?"} Sites -->|"Portrait / ID-doc<br/>FlowService.js:2943"| Compare Sites -->|"Videochat-close hook<br/>faceRecognitionHooks.js:41"| Compare Sites -->|"Self-service V1"| Compare Gate -->|No| Skip["NO faceComparisons row<br/>(liveness step silently absent)"] Gate -->|Yes| Compare["compareFaceDetections()<br/>FaceRecognitionService.js:213"] Compare --> Create["createFaceComparisonModel()<br/>:140-164"] Create --> Store[("faceComparisons row<br/>status + euclideanDistance<br/>stored UNCONDITIONALLY")] Store --> ReadTime["Read-time verdict:<br/>SelfServiceCheckerService<br/>.getFaceComparisonResult() :132"] ReadTime --> Verdict{"euclideanDistance ><br/>all thresholds?"} Verdict -->|Yes| Diff["different_face<br/>(CHECK_FAILURE)"] Verdict -->|No| Match["match / probable / perfect"] style Store fill:#264653,stroke:#2a9d8f,color:#fff style Skip fill:#3d2020,stroke:#a85,color:#fff style Diff fill:#3d2020,stroke:#a85,color:#fff style Gate fill:#2d2d2d,stroke:#888,color:#fff style Verdict fill:#2d2d2d,stroke:#888,color:#fff
Comparison Call Sites (4)
| # | Step | Location | Notes |
|---|---|---|---|
| 1 | Liveness V2 | server/service/SelfServiceV2Service.js:1390, inside saveLivenessCheckV2Messages (:1308-1412) | Gated at :1375 by if (task.options?.recognitionOptions?.compareFaceWith). If a client’s flow proto liveness task does not set compareFaceWith, no liveness FaceComparison row is written. The base V2 proto (self-service-v2-phase-1.flow.proto.js:78-86) does not set it. |
| 2 | Portrait / ID-document | server/flow/FlowService.js:2943, inside handleTaskRecognitionOptions (:2899-2949) | Path is server/flow/FlowService.js — not server/service/. |
| 3 | Videochat-close hook | server/faceRecognition/faceRecognitionHooks.js:41 | Operator flow, not the mobile branch. |
| 4 | Self-service V1 | via the same compareFaceDetections | — |
Liveness rows are conditional
Because of the gate at
SelfServiceV2Service.js:1375, the liveness-step comparison only produces afaceComparisonsrow when the client’s flow proto explicitly setscompareFaceWithon the liveness task. Confirm the customer’s deployment proto before promising liveness-step data exists.
Threshold Resolution
different_face depends entirely on which thresholds apply. SelfServiceCheckerService.js:122-130:
- Per-room first — activity-log entry
selfService:v2:config:state→content.settings.faceComparison.euclideanDistances. - Fallback to global —
Settingtable, keyfaceComparison, fieldeuclideanDistances. - Code defaults (
:32-48):perfect: 0.5,match: null,probable: 0.6.
So different_face ≈ euclideanDistance > probable, i.e. > 0.6 by default — but a given room may override this, so confirm per deployment / per room before trusting the count.
Which Endpoints Filter by Status
| Endpoint | Filters status:'success'? |
|---|---|
server/web/routes/room.endpoint.js:145 (operator room view) | Yes |
selfservice-room.endpoint.js:320 | No |
selfserviceroom.endpoint.js:130 | No |
flow.endpoint.js:196 | No |
FaceComparisonService (server/service/FaceComparisonService.js:9-41) | Queries FaceRecognition with status:'success' |
The self-service endpoints do not filter by status. The real reason failures look “missing” is that different_face is derived, not stored — not that any endpoint hides them.
”A mobilos ág” — what the customer means
Not a git branch
“A mobilos ág” (the mobile branch / arm) = the Self-Service V2 mobile flow —
SelfServiceV2Serviceplus thecustomization/flow/self-service-v2-phase-1/protos. It is not a git or customization branch.
MVP Deliverable — Read-Only SQL
This is the answer to the customer’s request. Read-only, no release. Adjust different_face_threshold to the deployment’s actual euclideanDistances.probable and set the date range:
WITH params AS (
SELECT
0.6::float8 AS different_face_threshold, -- euclideanDistances.probable
'2026-04-01 00:00:00'::timestamptz AS range_start,
'2026-05-01 00:00:00'::timestamptz AS range_end
)
SELECT
fc.id AS face_comparison_id, fc."createdAt" AS compared_at, fc.status AS comparison_status,
fc."euclideanDistance" AS difference_value, fc."selfServiceRoomId", fc."roomId",
fc."customerId", fc."userId",
rf."imageCategory" AS from_image_category, rt."imageCategory" AS to_image_category,
rf.id AS recognition_from_id, rt.id AS recognition_to_id
FROM "faceComparisons" fc
JOIN params p ON TRUE
LEFT JOIN "faceRecognitions" rf ON rf.id = fc."recognitionFromId"
LEFT JOIN "faceRecognitions" rt ON rt.id = fc."recognitionToId"
WHERE fc."createdAt" >= p.range_start AND fc."createdAt" < p.range_end
AND fc.status = 'success' AND fc."euclideanDistance" IS NOT NULL
AND fc."euclideanDistance" > p.different_face_threshold
ORDER BY fc."createdAt" DESC;status = 'success' here means the comparison was computed successfully — these are exactly the rows where a different_face verdict is meaningful (a large but valid distance). status = 'failed' rows represent comparisons that could not be computed at all and are not different_face cases.
Caveats / Limitations
Record these caveats when handing the SQL to the customer
- Liveness-step rows are conditional — they exist only if the client flow proto sets
compareFaceWithon the liveness task (gate atSelfServiceV2Service.js:1375). The base V2 proto does not.- Threshold is overridable — per-room or global config can change
probableaway from the0.6default. Confirm the real value before trusting the result count.- No step column —
faceComparisonshas no portrait-vs-liveness discriminator. The only signal is the joinedFaceRecognition.imageCategory: ID-document categories → portrait step;customer-portrait→ liveness step.imageCategoryvalues are themselves flow-config-dependent, so treat this as a heuristic, not a guarantee.euclideanDistanceholds cosine distance (range 0–2) despite the column name. The threshold is interpreted in the same space.
Corrections to Prior Reference Docs
Stale facts in the FaceKom triage agents'
_shared-context.md
- It states the model import hub is
server/db/models.ts— it is actuallyserver/db/models.js(plain JS).FlowService.jsis underserver/flow/, notserver/service/. These should be corrected in_shared-context.mdso future triage doesn’t chase the wrong paths.
The Customer’s “Previous Ticket”
The customer referenced a prior ticket containing working SQL for successful comparisons. That ticket was not found in any local store — it is likely YouTrack-only. The support engineer should pull it from YouTrack; it would confirm exactly which schema/columns the customer is already comfortable with.
Related
- vuer_oss — host service;
faceComparisons/faceRecognitionstables,FaceRecognitionService,SelfServiceV2Service,FlowService - vuer_cv — computer-vision service producing the recognition results compared here
- database-schema — Postgres schema reference
- FaceKom — platform overview
- Related tickets: FKITDEV-8752 (KH face-match logic rework, PR #7914), FKITDEV-6437 (face comparison limit perfect→probable), FKITDEV-7166 (face compare mods), FKITDEV-8484 (unicredit liveness v2), FKITDEV-8145 (liveness check v2)