자동화

검색 API를 직접 만들었다 — SearXNG에 키 로테이션과 본문 추출 붙이기

유료 검색 API의 무료 한도는 금방 마른다. SearXNG를 소스로 띄우고 여러 무료 API 키를 돌려쓰는 자체 검색 서버를 만든 과정과 설계 판단을 정리했다.

AI 에이전트에 검색 기능을 붙이려고 하면 바로 벽에 부딪힌다. 검색 API는 대부분 유료고, 무료 한도는 월 1,000~2,000회 수준이다. 에이전트 하나가 리서치 작업 한 번 돌리면 수십 번 검색한다. 한 달을 못 간다.

키를 여러 개 만들어 돌려쓰면 되지 않을까 싶지만, 그러려면 어떤 키가 얼마나 남았는지 추적해야 하고, 소진되면 자동으로 다음 키로 넘어가야 하고, 프로바이더마다 한도 정책이 다르다. 손으로 관리할 수 있는 일이 아니다.

그래서 검색 서버를 직접 만들었다.

무엇을 만들었나

한 문장으로 하면, 여러 검색 소스를 하나의 API로 묶고 키를 자동으로 돌려쓰는 서버다.

[외부 클라이언트 / 앱 / WireGuard 피어]
              │ HTTP
              ▼
┌──────────────────────────────────────────────┐
│  Search API Server (FastAPI)                  │  ← 외부에 노출되는 유일한 지점
│  - /api/search (links/content/answer)          │
│  - /api/health, /api/keys/status               │
│  - /dashboard (키 상태 + 처리 로그)              │
└─┬────────┬────────┬────────┬──────────┬───────┘
  ▼        ▼        ▼        ▼          ▼
┌─────┐ ┌──────┐ ┌────────┐ ┌──────┐ ┌─────────┐
│Brave│ │Tavily│ │Serper  │ │Jina  │ │Local LLM│
│     │ │      │ │SerpAPI │ │Reader│ │(vLLM)   │
└─────┘ └──────┘ └────────┘ └──────┘ └─────────┘
   (API 키 로테이션)          (본문)     (답변)
            │
            ▼
     ┌──────────────┐
     │  SearXNG     │  ← loopback 전용, 외부에서 직접 접근 불가
     └──────────────┘

SearXNG를 Docker 없이 서브모듈로 띄운 이유

SearXNG는 여러 검색엔진의 결과를 모아주는 메타 검색엔진이다. 보통은 Docker로 띄운다. 나는 git submodule로 소스를 받아 네이티브로 빌드하는 쪽을 택했다.

Docker를 피한 이유는 단순하다. 이 서버는 이미 GPU 작업이 도는 머신에서 함께 돌아간다. 컨테이너 런타임 레이어를 하나 더 얹으면 디버깅할 때 확인할 곳이 하나 늘어난다. 로그를 보려면 컨테이너 안으로 들어가야 하고, 포트 매핑을 한 번 더 신경 써야 한다.

서브모듈 방식의 실질적 이점은 업데이트가 명시적이라는 것이다.

cd searxng && git pull origin master && cd ..
./setup.sh           # 의존성 재설치
git add searxng      # 부모 저장소의 gitlink 갱신

git add searxng로 커밋되는 건 SearXNG 소스 전체가 아니라 커밋 해시 하나다. 그래서 “지금 우리 서버는 SearXNG의 어느 시점 버전으로 돌고 있는가”가 부모 저장소 히스토리에 그대로 남는다. 문제가 생기면 그 해시로 되돌리면 된다.

키 로테이션: 월 한도와 일 소프트 캡을 같이 본다

키 로테이션에서 처음 만든 버전은 단순했다. 키가 429를 뱉으면 다음 키로 넘긴다.

이게 잘 안 됐다. 이유는 두 가지였다.

첫째, 429를 받고 나서 넘기면 이미 그 요청은 실패한 뒤다. 사용자는 응답을 기다리다 에러를 받는다. 재시도를 넣으면 응답이 느려진다.

둘째, 프로바이더가 월 한도를 하루에 다 쓰는 걸 막지 않는다. 월 2,000회짜리 키를 첫날에 다 쓰면 남은 29일은 그 프로바이더가 없는 것과 같다.

그래서 사용량을 미리 추적하는 쪽으로 바꿨다.

  • 프로바이더별로 최대 12개까지 키를 등록한다
  • 월 한도와 일 소프트 캡을 동시에 본다
  • 요청 전에 남은 쿼터를 확인하고, 부족하면 그 키를 아예 고르지 않는다
  • 사용량은 SQLite에 저장한다. 서버를 재시작해도 카운트가 살아남는다

일 소프트 캡을 “소프트”라고 부르는 이유는 넘어도 즉시 차단하지 않기 때문이다. 다른 키가 전부 소진된 상황이면 캡을 넘겨서라도 응답하는 편이 낫다. 한도를 아예 못 넘게 만들면 정작 필요할 때 서버가 아무 답도 못 한다.

검색 모드를 네 단계로 나눈 이유

같은 검색이라도 필요한 깊이가 다르다. 링크 목록만 있으면 되는 경우와, 본문을 읽고 답까지 필요한 경우는 비용이 열 배 넘게 차이 난다.

모드 하는 일 언제 쓰나
links 링크와 스니펫만 기본값. 가장 싸고 빠르다
content 상위 N개 URL 본문을 Jina Reader로 추출 실제 내용을 읽어야 할 때
answer 본문 + 로컬 LLM 답변 생성 질문에 바로 답이 필요할 때
answer_v2 멀티 쿼리 + chunk rerank 다단계 RAG 어려운 질문

기본값을 links로 둔 게 중요했다. 기본을 answer로 두면 호출하는 쪽이 별생각 없이 쓰다가 LLM 추론 비용이 계속 나간다. 비싼 동작은 명시적으로 요청해야 하게 만드는 편이 안전하다.

네트워크 바인딩: 외부에 여는 건 하나뿐

이 부분은 처음부터 규칙을 정해두고 시작했다.

  • SearXNG는 loopback에만 바인딩한다. 외부에서 직접 접근할 수 없다
  • 외부에 열리는 건 API 서버 하나뿐이다. Bearer 토큰 인증을 건다
  • 다른 기기에서 써야 하면 0.0.0.0 전체 공개가 아니라 WireGuard 인터페이스 IP에 바인딩한다
  • 0.0.0.0 바인딩은 별도 환경변수(ALLOW_PUBLIC_BIND=0)로 기본 차단해뒀다

메타 검색엔진을 외부에 열어두면 남이 내 서버를 통해 검색을 돌린다. 내 API 키 쿼터가 남의 검색에 쓰인다. 실제로 당하기 전에는 잘 와닿지 않지만, 열어두면 결국 발견된다.

대시보드가 생각보다 많이 쓰였다

만들 때는 부가 기능이라고 생각했는데, 실제로 가장 자주 여는 화면이 됐다.

키 상태(녹색/붉은색), 프로바이더별 사용량, 검색 처리 로그를 한 화면에서 본다. 검색이 느려졌을 때 “어느 프로바이더가 죽었나”를 바로 확인할 수 있다. 로그를 grep하는 것보다 훨씬 빠르다.

교훈이라면, 자동으로 돌아가는 시스템일수록 상태를 눈으로 볼 방법이 필요하다는 것이다. 자동화는 잘 돌 때는 안 보이지만, 안 돌 때도 안 보인다는 게 문제다.

정리

  • 검색 API 무료 한도는 에이전트 워크로드를 감당하지 못한다. 로테이션이 필요하다
  • 429를 받고 넘기지 말고, 쿼터를 미리 추적해서 고르지 않는 쪽이 낫다
  • 월 한도만 보면 첫날에 다 쓴다. 일 단위 캡이 같이 있어야 한다
  • 비싼 모드는 기본값으로 두지 않는다
  • 메타 검색엔진은 loopback에만 묶고, 외부 노출 지점은 하나로 줄인다