230만 장 규모의 이미지(레딧 덤프 기반)에서 밈·이미지 검색을 하기 위해, 단일 서버에서 스트리밍 임베딩·온디스크 ANN 인덱스(DiskANN류)·품질 분류기 등을 직접 구현하며 병목을 줄이고 자원 사용을 최적화한 과정과 기술적 선택들을 정리한 글
새 검색 시스템은 여기에서 사용해 볼 수 있다. 기존 Meme Search Engine을 대체할 생각은 없다. 기존 엔진의 더 잘 큐레이션된 데이터셋이 대부분의 응용에서는 여전히 더 유용하기 때문이다.
전에 아무도 안 할 거라고 생각조차 못 했던 일을, 가장 먼저 안 하는 사람이 되라.
— 브라이언 이노(Brian Eno)
컴퓨터는 아주 빠르다. 요즘 컴퓨터가 일상적으로 굼뜨게 느껴지고, 많은 엔지니어들이 두텁게 추상화된 클라우드 시스템 위에서 일하고 있기 때문에 이 사실을 잊기 쉽다. 하지만 지금 내가 쓰는 약간 구식 노트북조차, 이따금 의미 없이 버벅거리고 아무 일도 하지 않는 동안, 코어당 초당 150억 개 정도의 명령을 실행할 수 있다. 가끔 "하루에 수백만 건의 요청을 처리해야 하는" 시스템 얘기를 듣지만, 하루는 대략 10^5초이고, 지금보다 훨씬 열악한 하드웨어에서 초당 수십 쿼리를 처리하는 문제는 수십 년 전에 이미 해결되었다.
GPU 쪽은 상황이 더 우습다. 요즘 소비자용 GPU 한 장이 1990년대의 슈퍼컴퓨터 전체와 비슷한 속도를 내는데[1], 실제로는 대부분 게임에서 삼각형 셰이딩하는 데 쓰이고 있다. Production Twitter on One Machine, Command-line Tools can be 235x Faster than your Hadoop Cluster, Marginalia 같은 프로젝트의 정신을 이어, 나는 내 단 하나뿐인 "서버"[2]에서, 가능한 한 "아무 일도 하지 않기"를 신중히 실천함으로써, 경쟁력 있는 크기의 이미지 데이터셋과 검색 시스템을 구축했다.
이 프로젝트의 기본 아이디어는 5월쯤에 떠올랐다. 기존의 반(半)수동 큐레이션 시스템 없이 더 많은 밈과 더 일반적인 컬렉션을 확보할 수 없을까 고민하던 중이었다. 특히 오픈 도메인 이미지 검색을 제공하고 싶었다. MemeThresher의 크롤러는 소수의 서브레딧만 긁어 오는데, 이걸 그냥 r/all[3]로 바꾸면 최신 데이터의 꽤 괜찮은 샘플을 얻을 수 있을 것 같아 보였다.
하지만 레딧이 IPO를 하고, 또는 어떤 관리자가 "사람들이 비정형 텍스트 데이터에 기꺼이 돈을 낼 수 있다"는 사실을 터무니없이 늦게 깨달은 이후, 레딧은 이제 스크레이핑을 원하지 않는다. 실제로 몇 천 개 항목 정도 긁다 보면 항상 끊겨 버렸다. 다행히도, 레딧 CTO의 말을 빌리자면,
과거(다른 사람들이!) 배포했던 기존의 대량 데이터 솔루션에는 대체로 "무허가(unsanctioned)"와 "비트토렌트(bittorrent)" 같은 단어가 포함된다
BitTorrent를 통해 배포되는 무허가 데이터셋은 연구에서 널리 쓰이며, PushShift와 Arctic Shift가 부지런히 관리하고 있다. 이 데이터셋은 놀랍도록 쉽게 다운로드하고 사용할 수 있었다. 나는 스테이징 VPS(기계식 하드디스크 하나와 브로드웰 코어 두 개가 전부인…)에서 전체 500GB 분량의 과거 제출물 기록을 전부 압축 해제한 뒤, 스트리밍 방식으로 간단한 분석 도구를 통과시켰다. 대략적인 추정을 해 보니, 일부만이 아니라 모든 이미지(2024년 11월까지)[4]를 처리하는 것도 가능하겠다는 결론에 이르렀다.
처음에는 이게 직관에 반할 수 있다. 내가 당시 대략 잡아 본 "모든 이미지"의 수는 약 2억 5천만 장이었다. 이미지를 개당 (조금 비관적으로) 1MB로 잡으면, 분명 내게 250TB의 저장 공간은 없다. 제대로 쓸 만한 썸네일은 최고 수준의 압축을 써도 장당 50kB 정도 차지할 테고, 그렇게 만든다 해도 그 압축 과정이 꽤 비쌀 뿐 아니라, 12TB 정도의 용량이 필요하다. 내가 가진 여유 용량보다 많다.
요령은, 사실 그 어떤 것도 저장할 필요가 없다는 데 있다[5]. 검색을 위해 필요한 것은 임베딩 벡터[6]뿐이고, 이건 장당 약 2kB 정도 공간이면 된다(실용성을 위한 메타데이터는 조금 더 필요하다). img2dataset 같은 선행 작업은 나중에 임베딩을 만들기 위해 리사이즈된 이미지를 남겨 둔다. 나는 이 과정을 피하기 위해, 전체 시스템을 모놀리식 최소 버퍼링 파이프라인으로 구현했다. URL에서 이미지 버퍼로, 다시 임베딩으로, 그리고 아주 큰 압축 블롭으로 디스크에 직행시킨다. 이때 GPU에 먹일 만큼만 다운로드 속도를 조절하는 백프레셔도 넣었다.
나는 이걸 하루 이틀 정도 구현했고, 처음에는 이미지 중 일부만 임의로 샘플링하는 모드로 테스트했다. 이 과정에서 병목이 몇 개 드러났다. 특히 임베딩을 만드는 추론 서버가 이론적으로 가능한 속도보다 느리고, CPU를 상당히 많이 잡아먹고 있었다. 그래서 꽤 투박한 방식으로 모델을 AITemplate를 써서 다시 짜 보았다. 원래는 네트워크 대역폭 한계 근처에서 돌리는 상황을 예상했지만, GPU를 꽉 채우고 추론 서버를 개선한 뒤에도 처음에는 다운스트림 200Mbps 정도밖에 안 나왔다. 놀랍게도 더 중요한 제약은 CPU 기반 이미지 전처리 코드였다. 이건 이미지 품질을 아주 조금 희생하는 쪽으로 "고쳤다". 또, 터무니없이 많은 병렬 다운로드를 처리하기 위해 파일 디스크립터나 로컬 DNS 캐시 같은 여러 자원 한도도 늘려야 했다.
이 정도로도 대충 잘 돌아가긴 했지만, 좀 더 정교한 계산을 해 보니 전체 데이터를 한 번 도는 데 한 달 정도의 러닝 타임과 꽤 많은 추가 스토리지가 필요하다는 걸 깨달았다. 전기·SSD 비용도 무시할 수 없어서, 당시에는 프로젝트를 봉인해 두었다.
최근 우선순위를 재조정하면서 어차피 대용량 저장 공간이 많이 필요해진 김에, 이 프로젝트를 다시 꺼냈다. 메트릭 시스템과 통합하기 위한 마지막 손질, 이미지가 아닐 것 같은 URL을 더 이른 단계에서 걸러서 네트워크 트래픽 줄이기, 빠져 있던 일부 로그를 추가하기, 그리고 Imgur 갤러리 같은 링크를 (약간이나마) 처리하는 기능을 넣었다. 초기에 동시성 코드를 잘못 짠 탓에 레코드를 잘못된 순서로 읽으면서 재시작 후 복구에 실패하는 문제가 있었지만, 이걸 고치고 나니 며칠 동안 아주 매끄럽게 돌아갔다.
다만 메트릭 상에서 몇 가지 설명하기 어려운 불연속 구간이 보였고, 시간이 지날수록 점진적인 변화가 나타나면서 CPU 시간을 너무 많이 쓰는 상황이 되었다. 드디어 진지하게 최적화를 생각해야 했다.

시스템을 막 켠 직후의 메트릭 대시보드. 흰 줄은 "이미지 삭제됨" 플레이스홀더를 의미하는데, 나중에야 이걸 더 이른 단계에서 버리도록 했다.

CPU를 항상 100%까지 사용한 건 아니지만, GPU 활용도를 떨어뜨릴 만큼은 심각했다.
다행히도 쉬운 해결책이 있었다. 코드를 다시 살펴보니, 추론 서버의 Python 쪽에서 괜한 이유로 비효율적인 msgpack 직렬화 라이브러리를 쓰고 있었다. 이건 손쉽게 다른 걸로 교체 가능했다. 또, 네트워크 트래픽을 줄이려고 클라이언트가 추론 서버로 이미지를 전송할 때 PNG로 인코딩하도록 한 설정도, 이번 작업에는 전혀 필요 없었을 뿐 아니라, 인코딩/디코딩 때문에 CPU를 꽤 쓴다는 것도 깨달았다(PNG는 구식이고 느린 압축을 쓴다). Rust 이미지 리사이즈 코드 역시, 상당한 속도 향상을 주는 간편한 대체 라이브러리가 있었다.
이런 조치를 통해 CPU 부하를 100% 이하에서 안정적으로 유지할 수 있었지만, 시간이 지나면서 다시 조금씩 올라갔다. 추가 프로파일링 결과, 눈에 띄는 귀찮은 일거리 말고는 딱히 건드릴 만한 부분이 보이지 않았다. 그나마 console-subscriber라는 tokio 비동기 코드용 모니터링 도구가 런타임의 약 15%를 먹고 있는 걸 발견했다. 이걸 제거하고, 다시 이미지 리사이즈 품질을 약간 낮추니, 나머지 러닝 타임 동안은 모든 게 잘 돌아갔다. 나중에 네트워크 대역폭이 갑자기 치솟는 구간이 있었는데, 큰 PNG 파일이 평소보다 많이 몰려 있었기 때문으로 보였다. 이 때문에 프로젝트가 망하거나(최소한 20%는 느려지거나) 하지 않을까 걱정했지만, 몇 시간 지나자 자연스럽게 해결되었다.
이전 Meme Search Engine들은 수만 개 규모의 작은 데이터셋만 사용했기 때문에, 검색은 매우 쉬웠다. 그냥 무식한 브루트 포스로도 수 밀리초 안에 끝났고, 벡터 인덱스 역시 거의 블랙박스로 취급했다.
하지만 네 자릿수 규모의 확장은 여러 문제를 쉽게 만들어 주지 않는다. 브루트 포스 스캔은 몇 분씩 걸려 사실상 불가능하고, 근사 최근접 탐색(ANN) 알고리즘이 필요하다. 불행하게도, 내 요구 사항이 조금 특이해서, 난이도는 "사소함"에서 "활발한 연구 분야"로 점프했다. 내 데이터는 압축하지 않은 상태로 RAM에 들어갈 수 있는 용량보다 10배 많고, 적당히 높은 재현율[7]을 원하며, 지연 시간은 수백 ms 내에 맞추고 싶고, 이 분야의 연구자들에 비해 CPU 파워도 한참 부족하다. 거기에 텍스트 임베딩으로 이미지 임베딩을 검색해야 하는데, 이런 크로스모달 검색은 알려진 바에 따르면 ANN 인덱스 입장에서 훨씬 더 어려운 문제다.
"기본값"에 가까운 선택지는 FAISS의 inverted list이다(벡터를 클러스터링한 뒤, 일부 클러스터만 검색하는 방식; 클러스터는 디스크에 저장해 메모리 매핑할 수 있다). 여기에 product quantization(벡터를 손실 압축해 더 빠르게 스캔하고 메모리 풋프린트를 줄이는 방식)을 조합하는 게 보통이다. 처음에는 나도 이걸 쓸 생각이었다. FAISS 문서가 다소 제각각이긴 하지만, 일부 자료에서는 이 조합이 매우 잘 작동하는 것으로 나온다. 하지만 다른 자료를 보면, 내가 보기에 더 대표적인 데이터셋(다만 거기엔 데이터셋 크기가 나와 있지 않다)에서는 product quantization만 썼을 때 성능이 훨씬 나쁘다. 최근 연구[2305.04359]에서는 TEXT2IMAGE-100M, -1B 같은 비슷한 데이터셋에서 재현율이 끔찍하다는 결과를 보여주기도 한다.
더 나은 해결책도 있다. DiskANN은 온디스크 그래프 자료구조[8]를 사용하며, 재현율/초당 쿼리 수(QPS) 곡선에서 훨씬 좋은 성능을 보여준다. Rust 바인딩은 없지만, Rust 포트가 하나 있기는 하다. 문제는, 그 포트가 완성되지 않은 것처럼 보이고, 윈도우에서만 빌드되며, 구조가 과하게 복잡하고 이상한 설계 선택을 몇 가지 하고 있어서 신뢰하기 어렵다는 점이다.
대안으로, DiskANN C++ 코드를 Rust에서 직접 바인딩하는 방식을 고려해 보기도 했다. C++ 특유의 기행은 무시하고 쓰겠다는 마음가짐이었지만, 나는 C++를 좋아하지 않고, 이 코드 역시 다른 이상한 설계를 하고 있다[9]. 알고리즘 자체는 그렇게 어려워 보이지 않았기 때문에, 차라리 직접 구현하기로 했다. 그러면 온디스크 데이터 포맷을 바꿔, 벡터와 그래프 이웃뿐 아니라 결과 항목의 URL과 메타데이터까지 함께 넣을 수 있어, 지연 시간을 더 줄일 수 있을 것이다. 물론, 실제로 구현해 보니 상당히 어려웠다. 특히 내 CPU 연산 예산으로는 더더욱[10].
초기에는 많은 문제가 있었다[11]. 작은 테스트 데이터에 대한 인덱스도 처음에는 엉망이었고, product quantization의 중요성을 잘못 이해한 탓에 애초에 구현조차 하지 않았다. 그 뒤에는 아주 헷갈리는 버그도 몇 개 있었다. 말도 안 되게 많은 시행착오 끝에, 내부 버퍼에서 + i as u32를 깜박한 것과 어떤 불변식을 제대로 유지하지 않은 것이 원인임을 찾아냈다. 이걸 고치는 동안 스크레이퍼를 더 오래 돌리는 바람에 결과는 더 좋아졌으니, 결과적으로는 이득일지도 모른다.
작은 데이터셋에서 벤치마크해 보니, product quantization이 분포 밖(OOD) 쿼리 성능(그리고 분포 안(ID) 성능도 마찬가지지만, 그쪽은 시작점이 훨씬 높고 변동도 적다)의 큰 한계 요인이라는 게 드러났다. DiskANN 알고리즘은 그래프를 탐색하면서 전체 정밀도 벡터를 디스크에서 끌어오기 때문에, 디스크 읽기를 늘리면 재현율을 이론상 완벽에 가깝게 끌어올릴 수 있다. 하지만 디스크 읽기는 비싸다.
그래서 OOD-DiskANN의 AOPQ 알고리즘을 약간 변형해서 채택했다. 이 알고리즘은 쿼리 분포에 맞춰 양자화를 조정한다. 더 좋은 성능을 주장하는 RaBitQ도 잠깐 살펴봤지만, 실제로는 별로 좋아 보이지 않았고, 기본 아이디어가 다른 논문들에서 이미 검토된 뒤 거부된 것이기도 했다.
원론적으로는, 벡터를 짧은 코드로 더 효율적으로 압축하는 방법들이 많이 있다. 최근에는 신경망을 사용해 암묵적 코드북을 만드는 연구도 있다. 하지만 쿼리 시간당 product quantization 비교에 쓸 수 있는 예산이 약 1μs 정도뿐이라, GPU 오프로딩을 써도 이런 방식 대부분은 실용성이 없다[12].
유일하게 쓸 만한 건 additive quantization인데, 쿼리 시점 비용은 product quantization과 비슷하지만, 인덱싱 시점은 훨씬 느리다. DiskANN 같은 구조에선 너무 느려서 GPU 가속 없이는 실질적으로 쓰기 어렵다. 대부분의 additive quantizer는 빔 서치를 쓰는데, 이건 병렬화가 잘 안 된다. 다만 LSQ++ 정도는 현실적으로 쓸 수 있을 것 같다. 나중에 통합할 수도 있다.
예상치 못한 문제는 샤딩이었다. DiskANN은 메모리를 초과하는 데이터셋을 처리할 때, k-means 같은 알고리즘으로 데이터셋을 클러스터링하고, 각 샤드를 개별적으로 처리한 뒤 결과 그래프를 이어 붙인다. 하지만 k-means는 균형 잡힌 클러스터를 만들어 주지 않는다. RAM에 들어갈 수 있도록 최대 클러스터 크기를 제한해야 하기 때문에, 클러스터 크기 균형은 매우 중요하다.
이 문제는 비교적 저주받은 방식으로 해결했다. 데이터의 일부 샘플을 대상으로 시뮬레이티드 어닐링을 돌려 대략 적당한 클러스터 중심을 만들고, 전체 데이터셋을 실제로 샤딩할 때는 보정 항(가중치)을 추가해 이를 보다 균형 잡힌 분할로 보정하는 식이다. 이 외에도 여러 가지를 시도해 봤지만 실패했다.
전체 그래프 구성에는 42개의 샤드를 사용했고, 샤드당 약 1,300만 개의 벡터(각 벡터는 샤드 두 곳에 중복 저장해 서로 연결되도록 했다)를 처리하는 데 6일 조금 넘게 걸렸다. 인덱스에서 결과로 노출되지 않는 벡터도 있는데, 필터링을 너무 늦게 적용한 탓이다. 샤드를 나누고 합치는 보조 작업, 랭킹 모델 및 양자화기 학습 등도 있었지만, 이들은 훨씬 더 빨리 끝났다.
이 녀석들에 대해서는 할 말이 많아서, 따로 소절을 떼어야겠다. 겉으로 보기에는, 내가 원하는 걸 딱 해 줄 것 같은 기성 소프트웨어가 아주 많다. 즉, 벡터와 메타데이터를 저장하고 빠르고 효율적인 검색을 제공해 준다는 것이다. 하지만 실제로는 잘 동작하지 않는다.
분산 시스템에 더 익숙한 지인이 1년쯤 전에 이런 제품들을 전반적으로 검토했는데, 그 당시 모든 제품이 샤딩을 잘못 구현하고 있었다고 한다. 일반적인 데이터베이스에서는 각 키를 해시 등의 방법으로 단일 샤드(또는 복제를 위해 지정된 일부 샤드)에 할당해야 한다. 그러면 쿼리가 들어왔을 때 하나의 서버로만 보내면 된다. 하지만 지금의 Pinecone을 제외한 모든 벡터 데이터베이스는 쓰기 요청을 (해시로) 임의의 샤드에 보내고, 읽기는 모든 샤드에서 받아 병합한다. 이건 규모가 커질수록 성능이 나빠질 수밖에 없다. 대안은 벡터 기준으로 파티션을 나누는 것이지만, 이걸 하는 곳은 거의 없다.
이들 제품은 보통 비벡터 필터링과 검색을 제공한다고도 주장한다. 일반적인 관계형 DB에서 기대할 법한 기능이다. 예를 들어 "이 사용자로부터 온 레코드만 보여줘" 또는 "지난 일주일에 추가된 것만 보여줘" 같은 쿼리다. 하지만 이건 알고리즘적으로 어려운 문제이며, 아직까지 모두가 납득할 만한 답은 없다. 벡터 인덱스 구조 자체에 침투적인 변경을 해서 일부 쿼리를 효율적으로 처리할 수도 있고, 메타데이터의 각 값에 대해 별도 인덱스 샤드를 두거나, 벡터 top-k 결과를 먼저 읽고 그 뒤에 필터링할 수도 있다. 이들은 서로 다른 복잡한 트레이드오프를 갖고 있으며, 최종 사용자가 이를 이해하고 선택할 수 있어야 한다. 하지만 상용 벡터 데이터베이스는 이런 점을 거의 언급하지 않고, 내가 아는 한 대부분은 순진한 방법(사후 필터링)을 쓰거나, 메타데이터 파티셔닝을 한 값(예: 사용자)만 있다고 가정한다.
개인적인 추측으로는, 대부분의 벡터 데이터베이스에 서로 다른 사용자의 데이터를 섞어 넣으면 타이밍 사이드 채널 공격에 취약해질 수 있다고 본다. 다만 이걸 실제로 어떻게 악용할 수 있을지는 생각해 보기 어렵다.
컴퓨터 아키텍처 관점에서 수상한 선택도 많다. 몇몇 데이터베이스는 RAM을 기준으로 최적화된 그래프 인덱스를 사용하면서, 실제로는 이걸 디스크에서 조금씩 페이징해 읽는다. 당연히 이로 인한 지연 때문에 상당한 성능 저하가 발생한다. 심지어 값이 만만치 않은 일부 상용 서비스는 "서버리스"로 운영되는데, 이는 곧 "큰 지연에 민감한 인덱스를 원격 오브젝트 스토리지에서 읽어 오자"라는 뜻이다.
앞서 언급한 크로스모달 검색 문제 역시 마찬가지다. 대부분의 상용 서비스는 크로스모달 검색을 1급 기능으로 내세우지만, 이게 ANN 인덱스 입장에서 어떤 추가 어려움을 야기하는지, 이를 해결하기 위해 어떤 적응을 했는지에 대한 언급은 전혀 보이지 않는다(내가 찾아본 한에서).
이 현상에는 나름의 이유가 있다고 생각한다. 소프트웨어는 명시적으로든 암묵적으로든, 실제 사용처를 기준으로 만들어진다[13]. 현재 많은 수요가 있는 건 애매한 가치의 챗봇과 RAG(검색 증강 생성)이다. 그러다 보니 실제 응용은 대부분 애매한 가치의 챗봇이거나, 사용자마다 작은 데이터셋에 의미 기반 검색 기능을 제공하는 도구다. 챗봇 쪽은 QPS가 높지 않고, LLM이 더 느리기 때문에 지연 시간도 꽤 허용된다. 사용자별 작은 데이터셋을 다루는 경우에는, 해당 사용자의 인덱스를 필요할 때마다 페이지인하는 것도 합리적인 선택이다.
기본이 되는 벡터 인덱스 연구는 대부분 빅테크(내가 주로 활용한 연구는 마이크로소프트 리서치 쪽[14])나 대학에서 나오는데, 벡터 데이터베이스 제품을 실제로 내놓는 쪽은 상대적으로 제품/시스템 엔지니어링에 치우쳐 있고, 벡터 인덱스는 대체로 블랙박스로 취급한다.
당신, 독자가 벡터 데이터베이스가 필요하다면, 다음 세 가지 선택지가 가장 무난하다고 본다.
이들이 지원하는 규모를 넘어가는 순간, 당신은 결국 스스로 수렁에 들어가게 될 것이다.
인덱스는 필터링 및 중복 제거를 거친 덤프에서 생성되었다. 임베딩은 분류에 매우 잘 맞기 때문에, 여러 사이트에서 공통적으로 쓰이는 "이 이미지는 삭제되었습니다"류의 표준 플레이스홀더로 보이는 것들을 전부 버렸고(이 로직은 결국 스크레이퍼에도 넣었다), 추가적인 NSFW 필터링도 했다.
이미 인덱스가 없는 상태에서 중복 제거를 하는 건 꽤 까다로웠다. 이미지 인코더가 결정적이지 않기 때문이다[15]. 그래서 양자화된 벡터와 URL을 해시하는 방식으로 갔다. 이 방법은 분명 일부 중복을 놓쳤을 것이다. 게다가 스터전의 법칙에 따라, 세상 대부분은 형편없기 때문에, 이 상태로는 좋은 검색 결과를 기대하기 어렵다.
내가 이런 얘기를 할 때마다 "취향은 주관적인데…"라는 탄식이 따라붙곤 하지만, 나는 MemeThresher의 "좋음" 분류기를 다시 뜯어고쳐 재학습시켰고, 데이터셋의 모든 항목에 품질 점수를 부여해 재랭킹에 사용했다. 예전과 달리 품질 점수는 단일 스칼라가 아니라, "meme", "aesthetic", "useful" 세 가지 점수를 예측한다. 처음에는 이걸 더 세분화(강/약/무)해서 다시 한 번 학습했는데, 문제의 원인이 알고 보니 다른 곳에서 발생한 사소한 버그였다는 걸 나중에야 알았다.

이렇게나 어려운 결정을 내려야 한다.
이 과정에는 데이터 라벨링 지옥에서의 추가 노동과, 약간 다른 능동 학습(active learning) 알고리즘이 필요했다. 예전에 나는 이 글에서 몇 가지 제안을 했었는데:
So400m/14@384 on WebLI)보다 나은 공개 모델은 아직 없다. 공간 제약 때문에 지금도 일반 이미지 임베딩을 그대로 써야 했다.결론적으로, SigLIP 모델 자체가 이미 어느 정도 취향이 좋아서, 쿼리가 충분히 구체적이면 딱히 별짓을 하지 않아도 되고, 이 분류기가 주는 추가 가치는 생각보다 크지 않다. 일종의 "씁쓸한 교훈(bitter lesson)"이 실천된 셈이다.
예전에 나는 검색을 개선하기 위해 [SAE](스파스 오토인코더)를 실험해 봤지만, 지금의 시스템에선 거의 필요 없는 것으로 보인다. 그래도 흥미 차원에서 더 큰 SAE를 이미 학습해 두었다. 여기에서 다운로드할 수 있으며, 각 특성 방향별로 가장 긍정적/부정적인 샘플 페이지도 함께 제공한다. 주관적으로 보면, 음수 방향 샘플은 좀 더 일관된 경향이 있고, 특성은 더 구체적이다(특성 수는 65,536개에서 262,144개로 늘렸고, 학습 데이터로 쓴 임베딩 수는 10배 늘렸으며, 에폭은 1회만 돌렸다).
언제나 그렇듯, 로그와 데이터는 데이터셋 서버에서 확인할 수 있다.

밈 검색 마스터 플랜.