◂ feed-research2026-08-29 빌드

임베딩 기반 후보 생성

최종 갱신: 2026-08-09

"유저와 가까운 아이템을 벡터 공간에서 찾는다"는 한 문장 뒤에 서로 아주 다른 세 계열이 있다. X는 셋을 동시에 운영한다.

계열 벡터를 만드는 방법 X의 구현
그래프 분해 팔로우 그래프를 커뮤니티로 쪼개고 소속도를 벡터로 SimClusters
그래프 임베딩 이종 그래프의 엣지를 학습 목표로 TwHIN
Two-tower 유저탑·아이템탑을 따로 학습, 내적으로 채점 DeepRetrieval, Phoenix retrieval

Meta를 같이 놓으면 축이 하나 더 생긴다. X 자료에서 캘 수 있는 건 "벡터를 어떻게 만드는가"(인코더 계열)이고, Meta 자료에서 캘 수 있는 건 대부분 **"그 벡터를 어디에 어떻게 담는가"(인덱스)**다. 같은 질문의 서로 다른 절반이라 한쪽 문헌만 읽으면 다른 쪽이 통째로 안 보인다. 아래 X 3계열 → Meta 인덱스 스택 → 대조 순서로 읽으면 된다.

계열 1 — 그래프 분해 (SimClusters)

희소 벡터다. 차원이 커뮤니티 수이고 대부분 0이다. 학습이 아니라 커뮤니티 탐지로 만든다.

파이프라인 [확인] src/scala/com/twitter/simclusters_v2/:

  1. 팔로우 그래프에서 producer-producer 코사인 유사도를 만든다. producer 자격 minActiveFollowers, 상위 topK명. [확인] UpdateKnownFor20M145K2020.scala:65-66
  2. 이웃을 maxNeighbors로 자르고, 양방향 중 Max(score)로 대칭화. squareWeightsEnable이 켜져 있으면 엣지 가중치를 cos² × 10으로 바꾼다 — 이 10하드코딩이다(옆에 wtCoeff 파라미터가 따로 있는데 여기 안 쓰인다). [확인] UpdateKnownForSBFRunner.scala:321-339,409-413, UpdateKnownFor20M145K2020.scala:84
  3. Metropolis-Hastings 커뮤니티 탐지로 KnownFor(producer → 커뮤니티)를 만든다. 주 1회 실행(batchIncrement = Days(7)). [확인] UpdateKnownFor20M145K2020.scala:46
  4. InterestedIn = 팔로우행렬 × KnownFor → 유저 → 커뮤니티. socialProofThreshold = 2, maxClustersPerUser = 50. [확인] InterestedInFromKnownFor.scala:85-86,327-336
  5. 트윗 임베딩은 좋아요가 눌릴 때마다 그 유저의 InterestedIn 벡터를 더해서 실시간으로 누적한다. [확인] simclusters_v2/README.md:65-67, summingbird/storm/TweetJob.scala:123-207

상수를 인용하기 전에 — 코드 기본값과 실제 프로덕션 실행이 다르다

이 잡의 하이퍼파라미터는 전부 args.int("name", default) 꼴이라 코드에 보이는 값은 CLI 인자가 없을 때의 폴백이다. 그리고 같은 파일이 주석으로 실제 프로덕션 실행 명령을 적어놨는데, 두 개가 어긋난다. [확인]UpdateKnownFor20M145K2020.scala:144,155

파라미터 코드 기본값 주석의 프로덕션 명령 일치?
minActiveFollowers 400 :65 --minActiveFollowers 400
topK (producer 수) 20,000,000 :66 --topK 20000000
maxNeighbors 400 :69 --maxNeighbors 100 ❌ 4배
maxEpochs 3 :83 --maxEpochs 4
wtCoeff 10.0 :84 --wtCoeff 10.0 ✅ (단 위 2번에서 안 쓰임)

maxEpochs는 옆 파일 docstring도 "run the clustering algorithm for x iterations (x = 4 in the prod setting)"이라고 쓴다 [확인] UpdateKnownForSBFRunner.scala:50. 즉 두 개의 독립된 문서가 4를 가리키는데 코드 기본값만 3이다. 인용해야 할 값은 100과 4다 [추정].

중요한 사실 두 가지 더:

시간 감쇠가 계층마다 다르다 [확인]:

유저 취향은 느리게, 콘텐츠 인기는 빠르게 잊는다. 같은 시스템 안에서 시간 상수를 12,500배 다르게 잡았다.

SANN — 검색은 ANN이 아니라 역인덱스 스캔이다

이름은 "SimClusters ANN"인데 HNSW 같은 그래프 인덱스가 아니다. 커뮤니티 → 트윗 역인덱스를 브루트포스로 훑는다. [확인]simclusters-ann/.../ApproximateCosineSimilarity.scala

후보 클러스터 최대 50개 스캔 × 클러스터당 트윗 최대 200개 → 점수 누적 → 상위 N

[확인]maxScanClusters = 50, maxTopTweetsPerCluster = 200, SimClustersANNCandidateSource.scala:325-347

채점 모드가 4가지고 런타임에 고른다 [확인] :111-119:

프로덕션 default는 PairEmbeddingCosineSimilarity, minScore = 0.7, 후보 나이 상한 1.days. [확인] :325-347

희소 벡터라서 이게 성립한다. 0이 아닌 차원이 50개뿐이면 역인덱스 스캔이 그래프 인덱스보다 싸다. 조밀 벡터에는 못 쓰는 접근이다 [추정].

계열 2 — 그래프 임베딩 (TwHIN)

이종 그래프의 (lhs, relation, rhs) 트리플을 학습한다. TransE 계열인데 거리가 아니라 내적을 쓴다.

translated = x[:, 1, :] + trans_embs      # RHS에 관계 벡터를 더한다
score      = (x[:, 0, :] * translated).sum(-1)

[확인]the-algorithm-ml/projects/twhin/models/models.py:53-54,90

공개 학습 코드에 문제가 여럿 있다 — 그대로 재현하면 안 된다 [확인]:

프로덕션 차원 수는 공개 안 됐다. 공개된 유일한 설정은 장난감(embedding_dim: 4)이고 테스트 상수는 EMB_DIM = 128이다. [확인] local.yaml, test_models.py:16-17

서빙 쪽은 별개로 잘 드러난다 [확인]:

즉 TwHIN은 검색용이면서 동시에 랭킹 피처다. 임베딩을 두 군데서 재사용한다.

계열 3 — Two-tower (DeepRetrieval)

X 2025의 주력 OON 후보 생성기. 유저탑은 실시간 원격 호출, 아이템탑은 미리 계산해 벡터DB에.

유저 임베딩 = Hydra EmbeddingGenerationService gRPC 호출
아이템      = VecDB 컬렉션(사전 계산)
검색        = annClient.search(collection, vector, SearchParams(limit, scoreThreshold), filter)

[확인]DeepRetrievalUserEmbeddingQueryFeatureHydrator.scala:92-98, DeepRetrievalUserTweetANNCandidateSource.scala:77-84

u2i와 i2i가 호출 방식부터 다르다 [확인]:

지연 예산이 아주 빡빡하다. 유저 임베딩 생성은 요청당 150ms / 총 250ms, 재시도는 1%만. [확인] HydraEmbeddingGenerationServiceClientModule.scala:19-40

페이로드 필터가 벡터 검색에 붙는다category, isHighQuality, isHighNegEngRatio, tier. 즉 순수 최근접이 아니라 필터링된 ANN이다. [확인] DeepRetrievalUserTweetANNCandidateSource.scala:103-121

컬렉션이 최소 12개다 — tweet-deep-retrieval, tweet-deep-retrieval-exp3, content-exploration-deep-retrieval, content-exploration-text-emb, tweet-deep-retrieval-media, creator-incentive-retrieval-v1, refreshed-twhin-tweet 등. 여러 모델을 동시에 서빙한다. [확인] TweetMixerGlobalParams.scala:461-1149

학습 코드는 공개되지 않았다. tweet-mixer/에는 추론 호출만 있고 아키텍처·손실·음성 샘플링이 없다. [확인] — repo-wide

인덱스 계층 — 세 종류가 공존한다

인덱스 용도 파라미터
자체 HNSW (Scala/Java) 레거시 ANN 서비스 레포에 프로덕션 값 없음 — 아래 참조
FAISS SWIG 바인딩 IndexHNSWFlat/PQ/SQ
Qdrant TwHIN 전용 collectionName = "twhin-prod", topK = 100, timeout 20ms
VecDB (자체 thrift) DeepRetrieval 계열 SearchParams(limit, scoreThreshold) + payload filter
SANN 역인덱스 SimClusters 위 참조

[확인]TwHINANNCandidateSource.scala:76-78, ann/src/main/java/com/twitter/ann/faiss/swig/

HNSW 숫자를 인용하면 안 되는 이유. grep으로 잡히는 세 값이 전부 프로덕션 설정이 아니다 [확인]:

같은 함정을 2026 Rust 쪽에서도 봤다 — 모듈 트리에서 끊긴 스코어러 파일에 p::AUTHOR_DIVERSITY_DECAY 하드코딩 상수가 남아 있는 것. "grep으로 숫자가 잡힌다"와 "그 숫자가 돌고 있다"는 다른 얘기다.

모든 임베딩 검색 결과는 memcache로 감싼다. TTL 600초에 **지터 20%**를 준다 — randomizedTTL(ttl, earlyExpiration = 0.2) = ttl - ttl × 0.2 × rand(). 동시 만료로 인한 thundering herd 방지다 [추정]. [확인] Utils.scala:47-49

예외: 트윗-트윗 임베딩 ANN만 TTL 180초다. [확인] DeepRetrievalTweetTweetEmbeddingANNCandidateSource.scala:52

캐시 코드에 함정이 하나 있다. MemcachedCandidateSource에서 캐시가 "꺼진" 경로도 쓰기는 한다 — 읽기만 건너뛴다. [확인] MemcachedCandidateSource.scala:41-53


Meta — 재미있는 부분이 인코더가 아니라 인덱스에 있다

Meta의 retrieval 공개 문헌을 시간순으로 늘어놓으면 two-tower가 등장하는 건 중간 한 번뿐이고, 앞뒤로는 전부 인덱스 얘기다. 2020년에는 ANN을 기존 역인덱스 쿼리 언어에 밀어 넣었고, 2024~2026년에는 인덱스를 학습시키거나(HILL) 모델 텐서 안으로 접어 넣었다(SilverTorch). "무슨 임베딩을 쓰나"보다 **"인덱스를 무엇으로 볼 것인가"**가 이 계보의 질문이다.

계층 0 — EBR 2020: ANN을 별도 서비스로 빼지 않았다

[확인] Embedding-based Retrieval in Facebook Search, KDD 2020 (arXiv 2020-06). Facebook Search 배포 사례이지만 이후 Meta 전체 retrieval 문헌의 출발점이라 여기에 둔다.

기존 Boolean 역인덱스(Unicorn)에 최근접 이웃 연산자를 하나 추가했다:

(nn <key> :radius <radius>)

"By having the (nn) operator as part of our Boolean query language we can now support hybrid retrieval expressions, with arbitrary combinations of embeddings and terms."

(and (term "sports") (nn user_emb :radius 0.3)) 같은 식으로 term 매칭과 임베딩 검색이 한 쿼리 안에서 합성된다. 별도 ANN 서비스에 붙였다 떼는 구조가 아니다. 이유도 직접 밝힌다 [확인]:

"By implementing NN support in terms of pre-existing primitives, instead of writing a separate system, we inherited all the features of the existing system, such as realtime updates, efficient query planning and execution, and support for multi-hop queries."

radius 모드 vs top-K 모드 — 논문이 radius 쪽을 골랐다 [확인]:

"we found that radius mode can give better trade-off of system performance and result quality. One possible reason is that radius mode enables a constrained NN search (constrained by other parts of the matching expression) but top K mode provides a more relaxed operation which needs to scan the whole index to get top K results."

필터가 ANN 결과에 사후 적용되는 게 아니라 ANN 탐색 자체를 제약한다. X의 DeepRetrieval도 payload 필터를 거는 "필터링된 ANN"이지만(위 참조), 그건 벡터DB에 필터를 넘기는 형태고 Meta 2020은 필터가 곧 쿼리 언어다. 같은 문제의식에 대한 두 단계 다른 답이다 [추정].

양자화 튜닝 (전부 [확인], 같은 논문):

hard negative mining (전부 [확인]):

[추정] 이 마지막 문장이 실무적으로 제일 값나간다. "hard negative를 쓴다"는 서술만 있는 논문이 대부분인데 어느 랭크 구간에서 뽑는지가 결과를 가른다고 수치로 말한 몇 안 되는 1차 출처다.

계층 1 — SilverTorch (2025~2026): 인덱스를 모델 안 텐서로 접었다

[확인] SilverTorch 블로그, 2026-05-26 / 논문 HTML, arXiv 2025-11 초판

"Index as Model: Every retrieval component — the item index, eligibility filter, scoring layer and user tower — becomes a tensor or operator inside a single PyTorch model."

부품이 셋이다 (전부 [확인]):

부품 무엇 대체 대상
Bloom Index "a bloom-filter-based GPU indexing structure. It addresses the warp divergence of forward index, ensures contiguous memory access through bitwise operations" CPU 역인덱스 필터
fused Int8 ANN kernel "representing embeddings with 8-bit integers and leveraging GPU's dp4a instruction (computing four multiply-adds in one instruction)", "streams item embeddings directly from the embedding table ... avoiding intermediate tensor construction" Faiss-GPU
OverArch "a SilverTorch model includes additional scoring layers (referred to as the OverArch layer)", *"The OverArch implements Mixture of Logits (MoL)"* 후단 랭킹 일부

수치 (전부 [확인]): Bloom index 291–523× faster than CPU inverted index / fused Int8 ANN kernel 2.2–14.7× faster than Faiss-GPU / 전체 23.7× more requests per second, TCO 20.9×.

정책 필터가 GPU 커널 안으로 들어갔다는 게 핵심이다 [추정]. X는 필터를 벡터DB의 payload 조건으로 넘기고, Meta 2020은 쿼리 언어의 Boolean 절로 표현했고, SilverTorch는 모델 forward 안의 bitwise 연산으로 만들었다. 세 세대 모두 "필터를 어디에 두느냐"가 아키텍처를 결정한다.

인덱스 갱신 주기·스트리밍 in-place 갱신은 여기서 반복하지 않는다 → model-freshness.md.

계층 2 — MoL과 h-indexer: 내적을 버리되 인덱스로 벌충한다

[확인] Revisiting Neural Retrieval on Accelerators, KDD 2023 (Meta)

문제 제기가 명확하다: "dot product-based models produce low-rank recommendations", "dot products cannot capture complex user-item interactions, which are multifaceted and likely high rank." [확인]

"내적을 버렸다"가 아니라 "내적을 1단으로 강등했다"이다 [추정]. 표현력을 올리는 대가를 인덱스 계층 하나를 더 쌓아서 치른다. SilverTorch의 OverArch가 MoL을 구현한다는 문장과 이어 읽으면, MoL은 2023 논문에서 2026 프로덕션 서빙 스택으로 실제로 넘어간 것으로 보인다 [추정] — 다만 두 문서가 서로를 명시적으로 잇지는 않는다.

h-indexer는 MoL 논문(2407.15462)에 없다. 출처는 KDD 2023 쪽이다. [관찰] 2026-08-09 — 검사한 코퍼스는 arXiv 2407.15462의 v3·v4 HTML 전문 2건이고 h-indexer 문자열이 0회다.

계층 3 — RankGraph: co-engagement 그래프를 retrieval 인덱스로 쓴다

X의 SimClusters와 겨냥이 다르다 [추정]: SimClusters는 팔로우 그래프를 커뮤니티로 분해하고 주 1회 갱신하는데, RankGraph-2는 참여 그래프를 3시간마다 통째로 다시 만든다(→ model-freshness.md). 소셜 그래프 기반과 행동 그래프 기반의 차이가 그대로 갱신 주기 차이로 나온다. 다만 X도 TwHIN에서 참여 엣지를 쓰므로 "X=소셜 / Meta=행동"으로 단순화하면 틀린다.

계층 4 — Andromeda + HILL/MoNN: 인덱스를 학습시킨다

[확인] Meta Andromeda, 2024-12-02

"Andromeda organizes ads into a hierarchical index with multiple layers, reducing the number of inference steps by focusing only on most relevant nodes." "The hierarchical index and retrieval models are jointly trained, which aligns the index representations with neural networks; this improves both precision and recall compared to commonly used two-tower neural networks or approximate nearest neighbor search."

이 마지막 절이 이 노트에서 가장 중요한 한 줄이다. Meta가 two-tower + ANN을 자기가 이겨야 할 베이스라인으로 명시했다. 이 저장소가 "two-tower가 표준"이라고 적어온 프레임을 1차 출처가 직접 반박한다.

2026년에 논문으로 내부가 열렸다 [확인] Efficient Retrieval Scaling with Hierarchical Indexing for Large Scale Recommendation, Meta, arXiv 2026-04-14:

Andromeda·HILL·MoNN은 전부 광고 쪽 문헌이다. 유기적 피드 retrieval에 같은 스택이 쓰인다는 1차 진술을 찾지 못했다. [추정] — 검사한 코퍼스는 아래 출처 목록의 Meta 블로그·논문과 Transparency Center 시스템 카드 15종(2026-08-09 열람)이다. 미공개 자료까지 없다는 근거는 아니다. — 다만 facebook.md가 인용한 2023년 유기 쪽 블로그의 "a novel hierarchical deep neural retrieval architecture" 한 줄과 방향이 같아 계보상 연결로 읽을 여지는 있다 [추정].

계층 5 — Instagram: 서빙 비용이 아키텍처를 정한 사례

상세는 instagram.md에 있고 여기서는 왜 이 노트에 값나가는지만 적는다.

user.let(seed_id=user_id)
    .liked(max_num_to_retrieve=30)
    .account_nn(embedding_config=default)
    .posted_media(max_media_per_account=10)
    .filter(non_recommendable_model_threshold=0.2)
    .rank(ranking_model=default)
    .diversify_by(seed_id, method=round_robin)

[추정] account_nn(embedding_config=default)가 IG2Vec 계열 계정 임베딩 + 최근접 이웃이다 — 메서드 이름 자체가 account nearest-neighbor다. 그리고 이 쿼리는 계층 0의 하이브리드 사상과 같다: 임베딩 검색(account_nn)·정책 필터(filter)·랭킹·다양성이 하나의 retrieval 표현식 안에 들어 있다. Facebook Search 2020의 (nn ...) 연산자와 Instagram 2020의 IGQL이 독립적으로 같은 형태에 도달했다.

계층 -1 — Facebook 연결 콘텐츠에는 ANN이 아예 없다

[추정] 코퍼스가 이미 유저의 연결로 제한돼 있어 임베딩 검색이 필요 없다 → facebook.md. 비연결 쪽만 *"billions → thousands → a few hundred"*로 서술된다 [확인] ai.meta.com, 2023.

이게 두 회사 문헌량 격차의 근본 원인이다 [추정]: X는 팔로우 그래프가 얇고 OON 비중이 처음부터 커서 retrieval이 곧 제품이었고, Facebook은 2021년까지 연결 콘텐츠가 주력이라 retrieval 문제 자체가 작았다. 그 비중이 2021 11.7% → 2025 Q4 41.0%로 바뀌었고(facebook.md), Meta의 retrieval 문헌이 쏟아지기 시작한 시점과 겹친다.

케이스: Netflix — "retrieval 없음" 극단의 두 번째 표본

Facebook 연결 콘텐츠가 "그래프가 곧 후보 집합"이라 ANN이 없다면, Netflix는 "카탈로그가 곧 후보 집합"이라 ANN이 없다. PVR이 *"카탈로그 전체(또는 장르 필터된 부분집합)를 회원별로 정렬"*한다 [확인] TMIS 2015 — 코퍼스 수천~수만이면 브루트포스 전 채점이 가능해서 "가까운 것을 빨리 찾는" 문제 자체가 없다. ANN은 기법이 아니라 코퍼스가 수억이라는 조건의 산물임을 보여주는 대조군이다.

그래도 임베딩은 쓴다 — 용도가 다르다:

netflix.md

X vs Meta — 구조적 대조

X Meta
공개된 것 후보 소스의 이름과 상수 (소스 11개, maxScanClusters = 50, TTL 600초) 인덱스의 구조와 배율 (Bloom index 291–523×, Int8 kernel 2.2–14.7×)
후보 생성의 단위 이름 붙은 소스를 병렬 실행 후 concat 계층 인덱스를 한 번 타고 내려감
필터가 붙는 곳 벡터DB payload 조건 + 파이프라인 필터 17개 쿼리 언어 Boolean 절(2020) → GPU 커널 bitwise(2026)
인코더 SimClusters / TwHIN / two-tower 3계열 동시 운영 two-tower는 한 지면(IG Explore)의 선택지, ads 쪽은 명시적으로 그걸 이기려 함
재미있는 엔지니어링의 위치 인코더와 상수 인덱스
못 보는 것 학습 코드·손실·음성 샘플링 (DeepRetrieval 미공개) 유기적 피드의 소스별 후보 수 분해 (no primary source found)

[추정] 이 대조는 실제 시스템 차이라기보다 공개 채널 차이일 가능성이 크다. X는 코드를 던졌고 Meta는 논문과 블로그를 쓴다. 코드는 상수를 노출하고 논문은 배율을 노출한다. "X는 인코더 중심, Meta는 인덱스 중심"으로 결론내면 안 되고, "각자 공개한 절반이 다르다"까지가 안전하다.

다만 하나는 자료 형태로 설명되지 않는다 [추정]: Meta는 two-tower + ANN을 이겨야 할 베이스라인이라고 직접 썼고 X는 2025~2026에 two-tower(DeepRetrieval, Phoenix retrieval)로 옮겨갔다. 같은 시점에 반대 방향이다.


이 케이스에서 일반화할 수 있는 것

Meta 쪽을 붙이면 여기에 다섯 개가 더 붙는다.

누가 이걸 쓰는가

인덱스 재구축·재학습 주기(SilverTorch, RankGraph-2 3시간, Instagram 일 단위 아이템 타워)는 model-freshness.md에 정리돼 있다. 여기서 반복하지 않는다.

아직 확인 못한 것

X 쪽:

Meta 쪽:

출처

X

Meta — 인덱스 계층

Netflix

Meta — 서비스 쪽 근거 (인용 원문은 서비스 노트에 있다)