惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
有赞技术团队
有赞技术团队
罗磊的独立博客
博客园 - 叶小钗
Jina AI
Jina AI
博客园 - 司徒正美
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
Microsoft Security Blog
Microsoft Security Blog
大猫的无限游戏
大猫的无限游戏
量子位
MyScale Blog
MyScale Blog
V
Visual Studio Blog
博客园 - 聂微东
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
小众软件
小众软件
宝玉的分享
宝玉的分享

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
A dating algorithm that physically cannot read photos (an...
gyani · 2026-05-15 · via DEV Community

I have been writing a connection app for a year. Last week I open-sourced the matching engine, and the only design choice I want to walk through is the one that took the longest to talk myself into: the matcher does not have access to photos. Not "it ignores them." Not "it deprioritizes them." It cannot see them. The TypeScript build fails if you try.

If you only want the punchline, here it is.

// soulmate-core/src/rank.ts
export function rank(viewer: Profile, candidate: Profile): number;

type Profile = {
  prompts: PromptAnswers;       // five short text answers
  voice: VoiceTranscript;       // ~30 sec, kept as text
  intent: Intent;               // friendship | relationship | community
  meta: ProfileMeta;            // age band, city, language, etc.
};
// no photo field. anywhere on this type.

Enter fullscreen mode Exit fullscreen mode

The image bytes live in a different table, behind a different read path, behind a mutualVibe boolean. The function above has no reference to that table and no way to obtain one through normal app wiring. The constraint is enforced by the compiler.

The repo is at github.com/donnowyu/soulmate-core if you want to read along.

Why type-level, not flag-level

The natural shape of this is a feature flag. if (allow_photo_in_ranking) { ... }. Several products built on this shape. I think it is the wrong shape. Three reasons.

  1. Flags get flipped by people who weren't in the room when the principle was set. A future engineer, looking at the engagement dashboard on a tired evening, will propose a "secondary signal" A/B test. They will be right that the metric will move. They will be wrong that what is being measured is what we said we cared about. A flag does not survive that conversation. A type signature does.

  2. The constraint should live in the artifact, not the documentation. A README that says "do not use photos in ranking" is a memo. A type that has no photo field is a build error. Banks do not enforce referential integrity with memos.

  3. It is honest in a way I can verify in public. The repo is open. You can look at the entry-point type and convince yourself in 60 seconds. You do not have to take my word for anything.

The cost of doing it this way

I will not pretend this was free.

The most expensive part was the data model. I had to design the schema so that the photo entity has its own service, its own access control, its own read path. The image upload pipeline never returns to the matching service. The "show me a face" step is a separate request, gated server-side on the existence of a mutualVibe row keyed by both user IDs. That is not a refactor you do in an afternoon.

The second cost was deciding what Profile should contain so that ranking still works. I tried a lot of things. The current shape (five prompts plus a transcribed voice clip plus intent metadata) is the smallest set I found that produces matches I can defend on inspection. Most of a year was spent reducing it to that.

The third cost is a soft one. There is a class of user who, on the existing apps, sorts mostly by face. They will look at this product and bounce. That is fine. They were not the users I was trying to find.

The trick of the embedding

The text answers and the voice transcript get concatenated into a single document per user. That document is embedded into a 1536-dim vector. Ranking is cosine similarity over those vectors, with two soft rerankers (ideology distance, shared-passion overlap) breaking ties.

This is not exotic. The trick is not in the math. The trick is in the input. By construction, the model has never seen a pixel. By construction, the model has no learned latent dimension that correlates with attractiveness, because nothing in the training distribution ever encoded one. The rerank loop is small enough to read.

// rerank pseudocode
const baseline = cosine(viewerEmb, candidateEmb);
const ideologyPenalty = distance(viewer.ideology, candidate.ideology);
const passionBoost   = jaccard(viewer.passions, candidate.passions);
return baseline - 0.15 * ideologyPenalty + 0.10 * passionBoost;

Enter fullscreen mode Exit fullscreen mode

You can argue with the coefficients. I have. The coefficients are not the point of the post.

Why I am writing this on Dev.to

Because the type-system argument is the part of the project that is interesting to people who write code for a living, and because most of the press around "no photo dating apps" handles the question at the marketing layer, where it is much less interesting. The interesting question is whether the constraint is structural, and structural constraints are something a dev audience can read in source. I wanted that audience to be able to verify the claim without me in the room.

If you want the long-form essay version of this argument, it is on the product site at byvibration.com/essays/why-matching-layer-is-physically-blind. If you want the code, the repo link is at the top. If you want to push back on any of the choices, the comments are open and I will be in them.


I work on byvibration. The matching engine is open source. I am writing about it here because I think the type-signature framing is a transferable idea: constraints you want to honor across a long time should be expressed in the artifact, not the team's memory.---
title: A dating algorithm that physically cannot read photos (and why I wrote it that way)
published: false
canonical_url: https://byvibration.com/essays/why-matching-layer-is-physically-blind

tags: typescript, webdev, discuss, architecture

I have been writing a connection app for a year. Last week I open-sourced the matching engine, and the only design choice I want to walk through is the one that took the longest to talk myself into: the matcher does not have access to photos. Not "it ignores them." Not "it deprioritizes them." It cannot see them. The TypeScript build fails if you try.

If you only want the punchline, here it is.

// soulmate-core/src/rank.ts
export function rank(viewer: Profile, candidate: Profile): number;

type Profile = {
  prompts: PromptAnswers;       // five short text answers
  voice: VoiceTranscript;       // ~30 sec, kept as text
  intent: Intent;               // friendship | relationship | community
  meta: ProfileMeta;            // age band, city, language, etc.
};
// no photo field. anywhere on this type.

Enter fullscreen mode Exit fullscreen mode

The image bytes live in a different table, behind a different read path, behind a mutualVibe boolean. The function above has no reference to that table and no way to obtain one through normal app wiring. The constraint is enforced by the compiler.

The repo is at github.com/donnowyu/soulmate-core if you want to read along.

Why type-level, not flag-level

The natural shape of this is a feature flag. if (allow_photo_in_ranking) { ... }. Several products built on this shape. I think it is the wrong shape. Three reasons.

  1. Flags get flipped by people who weren't in the room when the principle was set. A future engineer, looking at the engagement dashboard on a tired evening, will propose a "secondary signal" A/B test. They will be right that the metric will move. They will be wrong that what is being measured is what we said we cared about. A flag does not survive that conversation. A type signature does.

  2. The constraint should live in the artifact, not the documentation. A README that says "do not use photos in ranking" is a memo. A type that has no photo field is a build error. Banks do not enforce referential integrity with memos.

  3. It is honest in a way I can verify in public. The repo is open. You can look at the entry-point type and convince yourself in 60 seconds. You do not have to take my word for anything.

The cost of doing it this way

I will not pretend this was free.

The most expensive part was the data model. I had to design the schema so that the photo entity has its own service, its own access control, its own read path. The image upload pipeline never returns to the matching service. The "show me a face" step is a separate request, gated server-side on the existence of a mutualVibe row keyed by both user IDs. That is not a refactor you do in an afternoon.

The second cost was deciding what Profile should contain so that ranking still works. I tried a lot of things. The current shape (five prompts plus a transcribed voice clip plus intent metadata) is the smallest set I found that produces matches I can defend on inspection. Most of a year was spent reducing it to that.

The third cost is a soft one. There is a class of user who, on the existing apps, sorts mostly by face. They will look at this product and bounce. That is fine. They were not the users I was trying to find.

The trick of the embedding

The text answers and the voice transcript get concatenated into a single document per user. That document is embedded into a 1536-dim vector. Ranking is cosine similarity over those vectors, with two soft rerankers (ideology distance, shared-passion overlap) breaking ties.

This is not exotic. The trick is not in the math. The trick is in the input. By construction, the model has never seen a pixel. By construction, the model has no learned latent dimension that correlates with attractiveness, because nothing in the training distribution ever encoded one. The rerank loop is small enough to read.

// rerank pseudocode
const baseline = cosine(viewerEmb, candidateEmb);
const ideologyPenalty = distance(viewer.ideology, candidate.ideology);
const passionBoost   = jaccard(viewer.passions, candidate.passions);
return baseline - 0.15 * ideologyPenalty + 0.10 * passionBoost;

Enter fullscreen mode Exit fullscreen mode

You can argue with the coefficients. I have. The coefficients are not the point of the post.

Why I am writing this on Dev.to

Because the type-system argument is the part of the project that is interesting to people who write code for a living, and because most of the press around "no photo dating apps" handles the question at the marketing layer, where it is much less interesting. The interesting question is whether the constraint is structural, and structural constraints are something a dev audience can read in source. I wanted that audience to be able to verify the claim without me in the room.

If you want the long-form essay version of this argument, it is on the product site at byvibration.com/essays/why-matching-layer-is-physically-blind. If you want the code, the repo link is at the top. If you want to push back on any of the choices, the comments are open and I will be in them.


I work on byvibration. The matching engine is open source. I am writing about it here because I think the type-signature framing is a transferable idea: constraints you want to honor across a long time should be expressed in the artifact, not the team's memory.