백엔드 데이터 인프라 75편 — RediSearch (FT.CREATE · FT.SEARCH · FT.AGGREGATE)

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 75편. RediSearch 모듈 — 전문 검색·세컨더리 인덱스·집계. FT.CREATE·FT.SEARCH·FT.AGGREGATE 명령어와 TEXT·NUMERIC·TAG·GEO·VECTOR 필드 타입, BM25 점수 모델, RedisJSON 통합, Elasticsearch 와의 비교까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 75편 — RediSearch (FT.CREATE · FT.SEARCH · FT.AGGREGATE)

이 글은 백엔드 데이터 인프라 시리즈 130편 중 75편이에요. 74편 에서 RedisJSON 으로 JSON 1급 처리를 잡았으니, 이번 75편은 RediSearch — 전문 검색·세컨더리 인덱스·집계까지 한 모듈로 묶은 도구를 다뤄요. 65편 secondary indexing 에서 자료구조 조합만으로는 풀리지 않던 한계를 본격적으로 풀어주는 카드입니다.

RediSearch가 어렵게 느껴지는 이유

기존 Redis 자료구조 패턴과는 모델 자체가 달라요. 명령어가 FT. 접두사로 따로 떨어져 있고, 인덱스·스키마·쿼리 언어가 새로 등장하니까 처음엔 다른 데이터베이스를 한 칸 더 얹는 느낌입니다.

첫 번째 벽은 인덱스 스키마예요. FT.CREATE 로 필드별 타입을 지정하고 PREFIX 로 어떤 키를 인덱싱할지 선언하는데, RDB의 CREATE INDEX 처럼 단순 인덱스가 아니라 문서 모델 위에 역인덱스(inverted index, 단어→문서 매핑 자료구조) 가 얹힌 구조라 한 단계 더 추상적입니다.

두 번째 벽은 검색 쿼리 syntax(쿼리 작성 문법). @field:value @price:[100 200] @tags:{tag1|tag2} 처럼 RediSearch 전용 쿼리 언어인데, Elasticsearch DSL(Domain Specific Language, 검색 쿼리 전용 JSON 문법) 도 아니고 SQL 도 아닌 새 모양이라 손에 익기까지 시간이 좀 걸려요.

이 글에서 RediSearch 의 인덱스 생성·5가지 필드 타입·쿼리 syntax·집계·RedisJSON 통합·Elasticsearch 비교까지 한 번에 훑습니다.

5가지 필드 타입

타입 용도
TEXT 전문 검색 (토큰화·stemming)
NUMERIC 범위 쿼리·정렬
TAG 정확 매칭 (카테고리·태그)
GEO 위경도 검색
VECTOR 임베딩 유사도 (77편)

TEXT 의 stemming(어간 추출, running→run 처럼 굴절 제거) 은 영어에서 강하고 한국어에서는 약해요. 이건 뒤 함정 절에서 다시 짚습니다.

FT.CREATE — 인덱스 생성

> FT.CREATE idx:products ON HASH PREFIX 1 "product:" SCHEMA \
    name TEXT \
    price NUMERIC SORTABLE \
    category TAG \
    location GEO
OK

ON HASH 는 Hash 키를 대상으로 한다는 뜻이고 (ON JSON 으로 바꾸면 JSON 키 대상), PREFIX 1 "product:"product:* 패턴의 키들이 인덱스 대상이라는 선언, SCHEMA 뒤로 필드 정의가 이어집니다.

JSON 대상:

> FT.CREATE idx:users ON JSON PREFIX 1 "user:" SCHEMA \
    $.name AS name TEXT \
    $.age AS age NUMERIC \
    $.city AS city TAG

AS <alias> 로 JSONPath 를 짧은 alias 로 받아 쓸 수 있어요.

FT.SEARCH — 검색

# 전문 검색
> FT.SEARCH idx:products "laptop"

# 필드 한정
> FT.SEARCH idx:products "@name:laptop"

# 범위 쿼리
> FT.SEARCH idx:products "@price:[100 500]"

# 태그 매칭
> FT.SEARCH idx:products "@category:{electronics}"

# 복합 (AND)
> FT.SEARCH idx:products "@name:laptop @price:[100 500]"

# OR
> FT.SEARCH idx:products "@name:laptop | @name:tablet"

# NOT
> FT.SEARCH idx:products "@name:laptop -@category:{accessories}"

# 정렬
> FT.SEARCH idx:products "*" SORTBY price DESC

# 페이징
> FT.SEARCH idx:products "laptop" LIMIT 0 10

쿼리 syntax 핵심:

  • @field:value — 필드 한정
  • [min max] — 범위 (NUMERIC), [(min +inf] = exclusive
  • {value} — 정확 매칭 (TAG)
  • | — OR
  • - — NOT
  • * — 모두
  • "exact phrase" — 정확한 구절

FT.AGGREGATE — 집계

SQL GROUP BY 와 거의 같은 자리에서 일해요.

# 카테고리별 개수
> FT.AGGREGATE idx:products "*" GROUPBY 1 @category REDUCE COUNT 0 AS count

# 카테고리별 평균 가격
> FT.AGGREGATE idx:products "*" GROUPBY 1 @category REDUCE AVG 1 @price AS avg_price

# 정렬 후 페이징
> FT.AGGREGATE idx:products "*" SORTBY 2 @price DESC LIMIT 0 10

reducer 는 COUNT·SUM·AVG·MIN·MAX·STDDEV·QUANTILE·COUNT_DISTINCT·FIRST_VALUE·TOLIST 로 SQL 의 집계 함수와 거의 1:1 대응됩니다.

BM25 점수 모델

전문 검색은 결과가 단순히 매칭/비매칭이 아니라 관련도 점수 로 순위가 매겨지는데, RediSearch 의 기본 점수 모델이 BM25(Best Matching 25, 정보검색 표준 랭킹 함수) 예요.

> FT.SEARCH idx:articles "redis tutorial" WITHSCORES
1) (integer) 2
2) "article:42"
3) "2.5"               # BM25 점수
4) ...
5) "article:99"
6) "1.8"

BM25 는 단어 빈도(term frequency)·역문서빈도(inverse document frequency)·문서 길이 보정을 조합한 점수로, Elasticsearch 의 기본 점수와 동일한 모델입니다.

RedisJSON 통합

74편 RedisJSON 에서 잠깐 본 자리예요.

> FT.CREATE bike-idx ON JSON PREFIX 1 bike: SCHEMA \
    $.name AS name TEXT \
    $.price AS price NUMERIC \
    $.colors AS colors TAG

> JSON.SET bike:1 $ '{"name":"Hyperion","price":1200,"colors":["red","blue"]}'
> JSON.SET bike:2 $ '{"name":"Phoenix","price":800,"colors":["green"]}'

> FT.SEARCH bike-idx "@price:[500 1000]"
1) (integer) 1
2) "bike:2"
3) ...

> FT.SEARCH bike-idx "@colors:{red}"
1) (integer) 1
2) "bike:1"
3) ...

JSON 문서 저장 + 풀텍스트 + 범위 + 태그 검색을 한 인스턴스에서 묶어 쓰는 그림 — MongoDB·Elasticsearch 스택을 따로 두지 않고 비슷한 경험을 Redis 하나에서 받는 셈입니다.

TAG 필드 — separator 주의

여기 시험 함정이 하나 있어요. TAG 의 기본 separator 는 , 라서 JSON 배열 ["red","blue"] 가 그대로 들어가면 RediSearch 가 콤마 기준으로 분리해 인덱싱해버려요.

> FT.CREATE idx ON HASH PREFIX 1 "p:" SCHEMA tags TAG SEPARATOR "|"
> HSET p:1 tags "red|blue|green"
> FT.SEARCH idx "@tags:{red}"

또는 separator 를 지정:

SCHEMA tags TAG SEPARATOR "|"

한계 — Elasticsearch 와의 비교

항목 RediSearch Elasticsearch
지연 시간 μs ms (~수십 ms)
처리량 매우 높음 높음
인덱스 크기 메모리 한계 TB 단위
풀텍스트 ◯ (BM25) (BM25·LMDirichlet 등 다양)
분석기 기본·custom 풍부 (수십 언어, 다양한 토크나이저)
한국어 형태소 △ (별도 설정) ◯ (nori 등)
운영 복잡도 낮음 (Redis 한 인스턴스) 높음 (cluster·노드 관리)
학습 곡선 낮음 높음
통계·집계 ◯ (더 풍부)
클라이언트 Redis 클라이언트 그대로 별도 라이브러리

선택

  • 빠른 검색 + 작은 인덱스 + 이미 Redis 있음RediSearch
  • 거대 인덱스 + 풍부한 분석기 + 복잡한 통계Elasticsearch / OpenSearch

설치 — Redis 7 이하 / Redis 8+

74편 RedisJSON 과 같은 자리예요. Redis Stack 또는 Redis 8+ 기본 빌드면 됩니다.

docker run -d -p 6379:6379 redis/redis-stack-server:latest

Spring Data Redis 통합

직접 raw 명령으로 보내도 되고, Spring Data Redis OM(Object Mapping, 객체-Redis 매핑 추상화) 로 한 단계 올려도 됩니다.

@Document
public class Product {
    @Id
    private String id;

    @Searchable
    private String name;

    @Indexed
    private Double price;

    @TagIndexed
    private Set<String> categories;
}

public interface ProductRepository extends RedisDocumentRepository<Product, String> {
    Page<Product> findByName(String name, Pageable pageable);
    Page<Product> findByPriceBetween(Double min, Double max, Pageable pageable);
}

Redis OM Spring 은 Spring Data 추상화 위에 RediSearch 를 얹어, 어노테이션만으로 인덱스·검색 메서드를 받아 쓰게 해줍니다.

한계·실무 함정

1. 인덱스 메모리 비용

인덱스 자체도 메모리를 써요. 큰 텍스트 필드를 인덱싱하면 원본과 역인덱스를 둘 다 들고 있게 됩니다.

2. 인덱스 재빌드 비용

스키마 변경은 인덱스 DROP 후 재생성이에요 (FT.DROPINDEX + FT.CREATE). 데이터셋이 크면 재빌드만 수 분이 걸립니다.

3. 한국어 토크나이저 약함

RediSearch 의 기본 토크나이저는 영어 중심이라, 한국어 형태소 분석은 Elasticsearch nori 가 훨씬 강력해요. RediSearch 의 한국어 검색은 stemming 단계에서 한계가 명확합니다.

4. TAG separator 함정

위에서 본 그대로예요.

5. NUMERIC 정렬은 SORTABLE 필수

SCHEMA price NUMERIC SORTABLE

SORTABLE 이 없으면 SORTBY 의 효율이 떨어집니다.

시험 직전 한 번 더 — RediSearch 함정 압축 노트

  • RediSearch = 전문 검색·세컨더리 인덱스·집계 모듈
  • 5가지 필드 타입 = TEXT·NUMERIC·TAG·GEO·VECTOR
  • FT.CREATE idx ON HASH/JSON PREFIX <n> <prefix> SCHEMA field TYPE ...
  • FT.SEARCH idx "query" — 검색
  • 쿼리 syntax = @field:value, [min max] (NUMERIC), {value} (TAG), | (OR), - (NOT), *, "exact phrase"
  • FT.AGGREGATE = GROUP BY + 집계 (COUNT·SUM·AVG·MIN·MAX·QUANTILE)
  • 점수 모델 = BM25 (Elasticsearch 와 동일)
  • WITHSCORES = 결과에 점수 포함
  • RedisJSON 통합 = ON JSON SCHEMA $.field AS name TYPE
  • TAG separator = 기본 ,, 변경 = SEPARATOR "|"
  • NUMERIC 정렬 = SORTABLE 필수
  • RediSearch vs Elasticsearch — μs vs ms / 메모리 한계 vs TB / 운영 단순 vs 복잡
  • 선택 — 빠르고 작음 = RediSearch / 거대 + 분석기 풍부 = ES
  • 한국어 형태소 = ES nori 가 강력
  • 설치 = Redis Stack (redis-stack-server) 또는 Redis 8+ 기본
  • 인덱스 재빌드 = FT.DROPINDEX + FT.CREATE
  • 큰 데이터셋 재빌드 = 수 분
  • 인덱스도 메모리 사용 — 원본 + 역인덱스
  • Spring 통합 = raw 명령 또는 Redis OM Spring (@Document·@Searchable·@Indexed·@TagIndexed)

공식 문서: RediSearch 에서 자세한 사양과 쿼리 syntax 를 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!