mooner:~/resume $ cat projects/amoa/README.md
AMOA
하이브리드 검색 백엔드
짧은 강의 클립 위에 플레이리스트, 후기, 레시피 공유, 추천, 통합 검색을 얹은 학습 서비스의 백엔드입니다. 백엔드를 혼자 맡아 API와 데이터 모델, 검색과 임베딩 색인 파이프라인을 만들고 있습니다.
mooner:~/resume $ cat projects/amoa/README.md
하이브리드 검색 백엔드
짧은 강의 클립 위에 플레이리스트, 후기, 레시피 공유, 추천, 통합 검색을 얹은 학습 서비스의 백엔드입니다. 백엔드를 혼자 맡아 API와 데이터 모델, 검색과 임베딩 색인 파이프라인을 만들고 있습니다.
mooner:~/resume/projects/amoa $ tree -L 1
Spring Boot로 시작해 Ktor와 Exposed로 다시 썼고, 지금은 기능 단위 수직 슬라이스 구조입니다. 기능마다 도메인이 포트를 갖고 인프라가 구현합니다.
route → application → domain ← infrastructure 방향만 허용하고 테스트로 막습니다.mooner:~/resume/projects/amoa $ cat docs/architecture.md
검색은 벡터 검색과 키워드 검색을 한 SQL 안에서 돌린 뒤 순위를 합칩니다(RRF). 임베딩은 요청 경로 밖에서, 아웃박스 큐를 도는 워커가 채웁니다.
pg_trgm 유사도로 순위를 매깁니다. 두 결과를 FULL OUTER JOIN으로 합쳐 RRF(k = 60)로 정렬합니다.FOR UPDATE SKIP LOCKED로 선점합니다. 실패하면 간격을 늘려 다시 시도하고 5번째에 멈춥니다.mooner:~/resume/projects/amoa $ git log --grep=fix
next_attempt_at을 "이 시각 전에는 보이지 않는다"는 한 가지 뜻으로 통일했습니다. 대기 중인 작업에는 재시도 시각, 선점한 작업에는 10분짜리 리스 만료 시각입니다. 조건 하나로 두 상태를 모두 선점하게 되고, 리스로 되살아난 작업도 시도 횟수를 씁니다. 이미 갇힌 행은 마이그레이션에서 되살렸습니다.ef_search)을 후보 수에서 계산했는데, pgvector의 상한 1000을 확인하지 않았습니다. 기본 요청(limit=20)이 ef_search = 1800이 되어 500이 났고, limit이 12 이상이면 모두 실패했습니다.ef_search가 아니라 후보 수 자체를 잘랐습니다. ef_search만 낮추면 반복 스캔이 늘어 오히려 느려진다는 측정(40일 때 p50 3.92ms, 200일 때 1.52ms)을 따른 결정입니다. 요청 크기 1–600과 오버페치 배수의 모든 조합에서 상한을 넘지 않는 것을 확인했습니다.halfvec으로 바꾸고(코사인 거리 최대 오차 7e-5), 앞 512차원으로만 인덱스를 만든 뒤 후보를 768차원으로 다시 정렬하는 2단 검색을 넣었습니다. 합성 데이터 2만 행에서 256차원은 recall@10 0.862로 기준(0.95)에 못 미쳤고, 512차원은 0.984였습니다./search와 /search/suggestions 둘로 정리하고 응답을 type으로 구분하는 형태로 바꿨습니다. 호환이 깨지는 변경 4건은 이때 한 번에 정리했습니다.when으로 두어, 새 종류를 빠뜨리면 컴파일이 막히고 등록을 빠뜨리면 스모크 테스트가 잡습니다.Accepting requestsAnsan --:-- KST
mooner:~/resume $ ./contact.sh