
Redis Vector Search로 구현하는 유사 플레이어 AI 매치메이킹 시스템
Redis Vector Search로 플레이 스타일 임베딩을 실시간 검색해 실력, 역할, 접속 품질이 비슷한 상대를 찾는 AI 매치메이킹 설계와 인덱스, 점수 보정, 운영 기준을 구현 예제로 정리합니다.
TL;DR Redis Vector Search는 플레이어의 최근 전적과 플레이 스타일을 벡터로 저장한 뒤 코사인 거리 기반 최근접 이웃 검색으로 비슷한 상대 후보를 빠르게 찾는다. 다만 벡터 유사도만으로 방을 확정하면 대기 시간과 네트워크 품질이 나빠질 수 있으므로 MMR·핑·파티·대기 시간 규칙을 최종 점수에 함께 반영해야 한다.
유사 플레이어 AI 매치메이킹이 해결하는 문제
전통적인 매치메이킹은 보통 MMR 하나를 중심으로 후보를 고른다. 이 방식은 실력 균형에는 효과적이지만 같은 MMR 안에서도 플레이 경험은 크게 다를 수 있다. 예를 들어 공격적인 플레이어와 방어적인 플레이어, 협동 목표를 우선하는 플레이어와 처치 수를 우선하는 플레이어를 같은 기준으로만 매칭하면 이탈률이 높아질 수 있다.
플레이어 AI 매치메이킹은 최근 경기 행동을 고정 길이 벡터로 표현하고 유사한 벡터를 검색한다. Redis Stack의 RediSearch 인덱스는 HNSW(Hierarchical Navigable Small World) 알고리즘으로 근사 최근접 이웃 검색을 제공하므로 매치 요청마다 전체 대기열을 순회하지 않아도 된다.
| 방식 | 주 기준 | 장점 | 한계 |
|---|---|---|---|
| MMR 버킷 | 레이팅 차이 | 구현이 단순함 | 플레이 성향을 반영하기 어려움 |
| 규칙 기반 큐 | 역할, 지역, 파티 | 운영 규칙이 명확함 | 규칙이 늘수록 조합이 복잡해짐 |
| 벡터 검색 | 행동 임베딩 유사도 | 복합적인 성향을 한 번에 비교 | 벡터 품질과 보정 규칙이 필요함 |

플레이어 임베딩에는 무엇을 넣어야 할까?
임베딩은 모델이 이해하기 쉬운 입력이면서 운영자가 설명할 수 있는 특성이어야 한다. 처음부터 수백 개의 원본 이벤트를 넣기보다 최근 20경기 기준의 집계 피처로 시작하는 편이 안전하다.
추천 피처 구성
| 그룹 | 예시 피처 | 전처리 |
|---|---|---|
| 실력 | MMR, 승률, 평균 KDA | 리그별 표준화 |
| 전투 성향 | 분당 피해량, 선제 교전 비율, 생존 시간 | 로그 변환 후 표준화 |
| 팀 기여 | 힐량, 오브젝트 참여율, 어시스트 비율 | 역할별 표준화 |
| 운영 성향 | 평균 경기 길이, 재접속 횟수, 항복 투표 비율 | 상한값 처리 |
| 환경 | 리전, 입력 장치, 평균 RTT | 필터 또는 별도 점수 |
벡터는 생성 뒤 L2 정규화한다. 코사인 거리에서는 방향이 유사도를 의미하므로 벡터 크기가 큰 특정 피처가 결과를 과도하게 지배하는 일을 줄일 수 있다.
임베딩 생성 모델은 두 단계로 접근할 수 있다.
- 초기 버전에서는 표준화한 수치 피처를 그대로 32~128차원 벡터로 사용한다.
- 충분한 매치 결과가 쌓이면 승패, 재매칭, 이탈, 신고를 목표값으로 하는 오프라인 모델을 학습한다.
- 모델 버전을 벡터 문서에 기록하고 서로 다른 버전의 벡터는 같은 검색 인덱스에서 섞지 않는다.
Redis Vector Search 인덱스는 어떻게 구성할까?
아래 예시는 64차원 FLOAT32 임베딩과 코사인 거리를 사용한다. region, mode, queue_state는 후보를 먼저 줄이는 TAG 필드이고 mmr은 실력 범위를 제한하는 NUMERIC 필드다.
FT.CREATE idx:match:players ON HASH PREFIX 1 player:profile: SCHEMA
region TAG
mode TAG
queue_state TAG
mmr NUMERIC SORTABLE
updated_at NUMERIC SORTABLE
profile_vector VECTOR HNSW 6 TYPE FLOAT32 DIM 64 DISTANCE_METRIC COSINE
HNSW는 정확한 전수 검색보다 메모리를 더 사용하지만 낮은 지연 시간에 적합하다. 큐 규모와 메모리 예산을 측정한 뒤 M과 EF_CONSTRUCTION을 조정한다. 검색 품질이 부족할 때는 먼저 EF_RUNTIME을 높여 보고 인덱스를 재생성해야 하는 구조 파라미터 변경은 별도 배포로 처리한다.
flowchart LR
A[경기 이벤트] --> B[피처 집계]
B --> C[64차원 벡터 정규화]
C --> D[Redis HASH 저장]
E[매치 요청] --> F[리전·모드·MMR 필터]
F --> G[HNSW KNN 검색]
D --> G
G --> H[핑·대기 시간·파티 보정]
H --> I[매치 확정]
Redis Vector Search로 매치 후보를 어떻게 찾을까?
검색은 같은 리전과 게임 모드, 허용 MMR 범위 안에서만 수행한다. 아래 쿼리는 최대 50명을 벡터 거리 순으로 가져온다. KNN 결과의 distance가 작을수록 더 유사하다.
FT.SEARCH idx:match:players
"@region:{ap-northeast-2} @mode:{ranked} @queue_state:{waiting} @mmr:[1450 1550]=>[KNN 50 @profile_vector $vec AS distance]"
PARAMS 2 vec $BINARY_FLOAT32_VECTOR
SORTBY distance
RETURN 5 mmr region updated_at distance queue_state
DIALECT 2
서버는 Float32Array를 바이트 버퍼로 전달해야 한다. 문자열 JSON 배열로 저장하면 벡터 필드 형식이 맞지 않고 검색 효율도 얻을 수 없다.
function toFloat32Buffer(values: number[]): Buffer {
const vector = new Float32Array(values);
return Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
}
const query = "@region:{ap-northeast-2} @mode:{ranked} @queue_state:{waiting} @mmr:[1450 1550]=>[KNN 50 @profile_vector $vec AS distance]";
const result = await redis.ft.search("idx:match:players", query, {
PARAMS: { vec: toFloat32Buffer(profileVector) },
SORTBY: { BY: "distance", DIRECTION: "ASC" },
RETURN: ["mmr", "region", "updated_at", "distance"],
DIALECT: 2,
});
벡터 거리만으로 매치를 확정하면 왜 문제가 될까?
가장 가까운 벡터가 항상 가장 좋은 상대는 아니다. 후보가 같은 파티에 속했거나 핑이 높을 수 있고 오래 기다린 플레이어를 계속 뒤로 미룰 수도 있다. 따라서 벡터 검색은 후보 생성에만 사용하고 확정 단계는 별도 점수로 처리한다.
- : Redis가 반환한 코사인 거리
- : MMR 차이가 작을수록 높은 점수
- : 예상 RTT가 낮을수록 높은 점수
- : 대기 시간이 길수록 높아지는 공정성 점수
가중치는 게임 장르와 지표에 따라 바뀐다. 경쟁 FPS는 핑과 MMR 비중을 높이고 협동 PvE는 역할 조합과 플레이 성향의 비중을 높이는 식이다.
매치메이킹 처리 절차
- 플레이어가 큐에 들어오면 서버가 최신 피처를 정규화하고
player:profile:{playerId}HASH에 벡터와 큐 상태를 저장한다. - 워커가 리전·모드·MMR 필터와 KNN 검색으로 후보 50명을 조회한다.
- 후보를 파티 충돌, 차단 관계, 핑, 대기 시간, 역할 규칙으로 재점수화한다.
- 원자적 예약에 성공한 플레이어만 매치를 만들고 나머지는 다음 주기에 다시 검색한다.
동시 요청에서는 두 워커가 같은 플레이어를 선택할 수 있다. 검색 결과를 신뢰해 바로 방을 만들지 말고 예약은 Lua 스크립트 또는 WATCH/MULTI로 원자 처리한다. 실무에서는 짧은 TTL의 match:reservation:{playerId} 키를 두고 큐 상태를 waiting에서 reserved로 전환하는 방식을 많이 쓴다.
if redis.call('GET', KEYS[1]) ~= 'waiting' then
return 0
end
redis.call('SET', KEYS[1], 'reserved', 'EX', ARGV[1])
return 1
이 예시는 플레이어별 상태 키 하나를 예약한다. 여러 플레이어를 한 매치로 예약할 때는 모든 대상 키를 정렬한 순서로 검사·변경해 부분 예약을 막아야 한다.
운영과 보안에서 확인할 항목
| 항목 | 최소 기준 | 이유 |
|---|---|---|
| 벡터 갱신 | 경기 종료 후 비동기 갱신 | 매치 요청 경로의 지연 시간을 보호 |
| 오래된 큐 제거 | updated_at 기준 만료 | 연결이 끊긴 유령 후보 제거 |
| 개인정보 | 원본 채팅·식별자 제외 | 행동 특징만으로도 재식별 위험을 줄임 |
| 접근 제어 | 매치 워커 전용 Redis ACL | 클라이언트의 직접 검색 차단 |
| 관측성 | 검색 지연, 예약 충돌, 재매칭률 | 품질 저하 원인 추적 |
| 롤백 | 모델 버전별 키 또는 인덱스 | 잘못된 임베딩 배포의 빠른 복구 |
특히 클라이언트가 벡터나 검색 필터를 직접 제출하게 두면 조작으로 유리한 상대를 찾을 수 있다. 피처 계산, Redis 조회, 매치 확정은 모두 신뢰 가능한 게임 서버에서 수행해야 한다.

자주 묻는 질문 (FAQ)
Redis Vector Search가 MMR 시스템을 대체하나요?
아니다. MMR은 실력 균형의 강한 기준으로 남기고 벡터 검색은 같은 실력대 안에서 플레이 성향이 맞는 후보를 찾는 보조 수단으로 사용한다.
HNSW 검색 결과는 항상 정확한가요?
아니다. HNSW는 속도를 위해 근사 최근접 이웃을 찾는다. 정답 품질이 중요한 경우 후보 수와 런타임 탐색 파라미터를 늘리고 실제 매치 품질 지표로 검증해야 한다.
벡터는 매치 요청마다 다시 계산해야 하나요?
보통은 아니다. 경기 종료나 일정 수의 이벤트 누적 시 비동기로 갱신하고 매치 요청은 이미 저장된 최신 벡터를 읽는 편이 지연 시간과 비용 면에서 유리하다.
정리
Redis Vector Search 기반 매치메이킹의 핵심은 벡터 검색으로 후보를 빠르게 좁히고 MMR·RTT·대기 시간·파티 규칙으로 최종 결정을 분리하는 데 있다. 먼저 해석 가능한 64차원 행동 피처와 작은 HNSW 인덱스로 시작한 뒤 매치 완료율·재큐율·이탈률을 기준으로 가중치와 임베딩을 개선하자.


