Elasticsearch 입문 13편 — Full-text Queries (match·match_phrase·multi_match·query_string)

2026-05-19Elasticsearch 입문에서 운영까지

Elasticsearch 입문 13편 Full-text Queries. match·phrase·multi_match·query_string·simple_query_string.

📚 Elasticsearch 입문에서 운영까지 · 13편 — Full-text Queries (match·match_phrase·multi_match·query_string)

이 글은 Elasticsearch 입문에서 운영까지 시리즈 38편 중 13편이에요. 12편에서 Search API 의 큰 그림을 잡았다면, 이번 13편은 그 안에서 진짜 풀텍스트 검색을 담당하는 쿼리 패밀리 를 한 호흡에 정리합니다.

📚 학습 노트

이 글은 Elasticsearch 8.x 공식 docs 의 Full text queries 섹션을 한국어 학습 노트로 풀어쓴 자료예요. match · match_phrase · multi_match · query_string · simple_query_string · match_bool_prefix · intervals 까지 한 통에 담았어요.

로컬 Docker ES + Kibana Dev Tools 에서 한두 번 직접 쳐 보면 본문이 훨씬 잘 박힙니다.

왜 Full-text Queries 가 ES 의 본 영역인가

10편(Analyzer) 에서 문자열이 토큰으로 쪼개지는 과정 을 봤고, 12편(Search API) 에서 Query DSL 의 큰 골격 을 봤어요. 13편은 그 둘이 만나는 자리예요 — 풀텍스트 쿼리 패밀리.

ES 쿼리는 크게 두 부류로 갈려요. Term-level 쿼리 (14편) 는 입력 문자열을 그대로 색인 토큰과 비교해요. 정확 일치 가 본질. 반면 Full-text 쿼리 는 입력 문자열을 해당 필드의 analyzer 로 한 번 더 통과시킨 다음 토큰들끼리 비교해요. 자연어 검색 이 본질이에요.

즉 풀텍스트 쿼리의 결과 품질을 결정하는 변수는 사실상 analyzer. match 쿼리에 같은 문장을 던져도 필드 매핑의 analyzer 가 standard 인지 nori 인지 whitespace 인지에 따라 맞는 문서 셋이 다 달라져요. 11편 한국어 analyzer 와 묶어서 보면 머리에 더 잘 박혀요.

이 글에서 다루는 쿼리는 다음 7개예요.

쿼리 자리 핵심
match 단일 필드 자연어 검색 analyzer 통과 + or/and operator
match_phrase 어구 검색 토큰 순서·인접성 보장
match_phrase_prefix 어구 + 마지막 단어 자동완성 prefix 매칭
multi_match 여러 필드 한 번에 type 6종
query_string Lucene 쿼리 문법 강력 / 위험
simple_query_string 사용자 안전 버전 파싱 에러 무시
match_bool_prefix / intervals 특수 케이스 자동완성·정밀 인접성

match — 가장 기본, 자연어 검색의 출발점

match 쿼리는 풀텍스트 검색의 디폴트 예요. 입력 문자열을 해당 필드의 search analyzer 로 한 번 통과시켜 토큰으로 쪼개고, 그 토큰들이 얼마나 많이 일치하느냐 로 점수를 매겨 정렬해요.

GET /products/_search
{
  "query": {
    "match": {
      "title": "무선 블루투스 이어폰"
    }
  }
}

내부 동작은 — "무선 블루투스 이어폰" 이 analyzer 를 통과해 [무선, 블루투스, 이어폰] 토큰 3개가 되고, title 인덱스에서 이 토큰 중 하나라도 들어간 문서를 모두 가져와요. 그 다음 얼마나 많은 토큰이 일치하는지 · 해당 토큰의 IDF 가 얼마나 큰지 · 필드 길이는 얼마나 짧은지 를 종합한 BM25 점수로 정렬해요.

핵심 옵션 세 가지가 있어요. operator 는 토큰들 사이의 결합 방식을 결정해요. 기본은 or — 하나만 맞아도 hit. and 로 바꾸면 모든 토큰 이 들어간 문서만 hit 가 되고 정밀도가 올라가요.

"match": {
  "title": {
    "query": "무선 블루투스 이어폰",
    "operator": "and"
  }
}

minimum_should_matchorand 의 중간 자리예요. "토큰 3개 중 최소 2개가 들어간 문서만" 같은 조건을 박을 수 있어요. "2", "75%", "3<-1 6<-2" 같은 식이 가능. 75% 는 토큰 수가 4개면 3개 일치 필요 라는 의미고, 3<-1토큰이 3개 이하면 모두 일치, 그 위면 1개 빠져도 OK 같은 복합 표현.

fuzziness오타 허용 자리예요. AUTO 또는 정수 1·2 를 박아요. Levenshtein 거리 (한 글자 추가·삭제·교체·인접 swap 1회를 1로 셈) 기준으로 거리 N 이내인 토큰까지 일치로 인정. AUTO3글자 이하 → 0, 4~5글자 → 1, 6글자 이상 → 2 같은 자동 조정이라 보통 이 값으로 시작해요.

"match": {
  "title": {
    "query": "이여폰",
    "fuzziness": "AUTO"
  }
}

위 예시는 "이여폰""이어폰" 으로 자동 보정해 일치를 잡아 줘요. 다만 fuzziness 는 비용이 비싸요 — 토큰 하나에 대해 변형 가능 후보 토큰 셋 을 모두 확인해야 해서, 큰 인덱스에서 무분별하게 켜면 응답이 수십 배 느려져요. 사고 5번에서 다시.

match_phrase — 어구 순서 그대로

match_phrase토큰 순서와 인접성 까지 따져요. "무선 이어폰" 을 던지면 [무선, 이어폰] 두 토큰이 원본 문서 안에서 이 순서로, 바로 옆에 나란히 등장해야 hit 가 돼요.

GET /products/_search
{
  "query": {
    "match_phrase": {
      "title": "무선 이어폰"
    }
  }
}

내부적으로는 inverted index 의 position 정보 를 사용해요. 10편(Analyzer) 에서 token 마다 position 이 같이 저장된다고 했죠 — match_phrase 는 그 position 들이 연속 인지를 확인해요. 그래서 index_options: positions 또는 offsets 가 매핑에 켜져 있어야 동작해요 (text 타입 기본값은 positions).

slop 옵션이 어구 검색의 유연성 다이얼 이에요. 기본값 0 이면 완전 인접 만 hit. slop: 2 로 박으면 토큰 사이에 다른 토큰 2개까지 끼어들어도 OK. 또 순서가 바뀌어도 swap 1회당 slop 2 를 소비하는 형태로 허용돼요.

"match_phrase": {
  "title": {
    "query": "무선 이어폰",
    "slop": 2
  }
}

이 쿼리는 "무선 노이즈캔슬링 이어폰" (사이에 1개) · "무선 블루투스 5.3 이어폰" (사이에 2개) 까지 잡아요. "이어폰 무선" (순서 swap) 도 slop 2 를 소비해 hit. slop 을 너무 크게 잡으면 사실상 match 와 같아지니까, 3~5 사이 가 실무 권장 범위예요.

match_phrase_prefix — 자동완성용 prefix

match_phrase_prefix마지막 토큰만 prefix 매칭 으로 풀어 주는 변종이에요. 사용자가 검색창에 "무선 이어" 까지 쳤을 때, "무선 이어폰" · "무선 이어버드" · "무선 이어셋" 까지 보여주는 자리.

GET /products/_search
{
  "query": {
    "match_phrase_prefix": {
      "title": {
        "query": "무선 이어",
        "max_expansions": 50
      }
    }
  }
}

max_expansions마지막 토큰의 prefix 가 확장될 수 있는 후보 토큰 수 상한 이에요. 기본 50. 이걸 너무 크게 잡으면 수만 개 후보 를 다 점수 계산해서 응답이 폭증해요.

자동완성 자리에서 권장 선택지는 사실 match_phrase_prefix 가 아니라 search_as_you_type 필드 타입 또는 completion suggester 예요. match_phrase_prefix 는 마지막 토큰이 적당히 긴 자리 (4~5글자 이상) 에서만 깔끔하게 동작하고, 짧은 prefix 에서는 후보 폭발이 심해요. 20편(Suggesters) 에서 깊이.

multi_match — 여러 필드 한 번에

상품 검색은 보통 title · description · brand · category 같은 여러 필드 를 한 번에 검색해요. 매번 6개 필드용 match 를 bool/should 로 묶는 건 번거로워서, ES 가 multi_match 라는 전용 단축형 을 줘요.

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "무선 블루투스 이어폰",
      "fields": ["title^3", "description", "brand^2"],
      "type": "best_fields"
    }
  }
}

title^3boost 3 배 라는 의미예요. 같은 토큰이 title 에 매칭되면 description 보다 3배 더 점수를 받아요. brand 는 boost 2, description 은 기본 1. 검색 결과 정렬을 어느 필드에서 매칭됐는지 로 통제하는 핵심 다이얼이에요.

type 이 multi_match 의 진짜 본체 예요. 6 종 중 어느 걸 고르냐에 따라 동일 쿼리도 결과가 완전히 달라져요.

best_fields (기본값) — 한 필드에 다 들어간 게 좋다

각 필드를 개별 match 로 돌린 다음, 가장 점수가 높은 단일 필드 의 점수를 최종 점수로 가져가요. "하나의 필드에 토큰이 다 들어간 문서가 더 가치 있다" 는 직관을 따라요. 상품 title 검색 같은 자리에 잘 맞아요.

tie_breaker 옵션으로 나머지 필드의 점수를 얼마나 섞을지 조절해요. 기본 0 이면 최고 점수 필드만 보고, 0.3 으로 박으면 최고 점수 + 나머지 점수 × 0.3 으로 약간 섞여요.

most_fields — 여러 필드에 골고루 들어간 게 좋다

각 필드 점수를 모두 더해 최종 점수로 가져가요. "여러 필드에 토큰이 분산돼 있는 문서가 더 가치 있다" 는 직관. 같은 텍스트를 여러 analyzer 로 색인한 multi-field 와 잘 어울려요 — 예 title (standard) + title.korean (nori) + title.english (english analyzer) 셋을 묶어 검색.

cross_fields — 여러 필드를 하나의 큰 필드처럼

여러 필드를 합쳐서 하나의 가상 필드 인 것처럼 다뤄요. 사람 이름 검색이 대표 자리 — first_name + last_name + middle_name 을 합쳐 "홍 길동" 을 검색하면 first_name 에 , last_name 에 길동 이 들어 있어야 hit. 토큰별로 최적의 필드 를 찾아 매칭하는 방식이라, operator: and 와 함께 쓸 때 각 토큰이 어느 필드에 있든 다 들어있으면 hit 라는 동작을 만들어요.

주의 — cross_fields 는 같은 analyzer 를 쓰는 필드끼리만 정상 동작해요. analyzer 가 다르면 분석 단계가 어긋나 결과가 망가져요. 그래서 multi-field 와 cross_fields 를 같이 쓸 때는 analyzer 옵션으로 명시 고정 하는 게 안전해요.

phrase — match_phrase 의 multi 버전

각 필드를 match_phrase 로 돌리고, best_fields 처럼 최고 점수를 가져가요. "무선 이어폰" 같은 어구를 여러 필드 에 어구 검색.

phrase_prefix — match_phrase_prefix 의 multi 버전

각 필드를 match_phrase_prefix 로 돌려요. 자동완성을 여러 필드 에서 동시에 잡고 싶을 때.

bool_prefix — match_bool_prefix 의 multi 버전

마지막 토큰만 prefix 로, 나머지는 일반 토큰으로 처리해요. 검색 중인 사용자 입력 처럼 아직 다 쳐지지 않은 쿼리를 다룰 때 잘 맞아요. search_as_you_type 필드 타입의 기본 검색 방식이기도 해요.

query_string + simple_query_string — Lucene 문법 두 갈래

query_stringLucene 의 강력한 쿼리 문법 을 통째로 노출해요. AND · OR · NOT · 괄호 · 와일드카드 · 정규식 · 필드 지정 · 부스트 까지 한 줄 문자열로 다 표현할 수 있어요.

GET /products/_search
{
  "query": {
    "query_string": {
      "query": "(무선 OR 블루투스) AND title:이어폰 NOT brand:노바디",
      "default_field": "description"
    }
  }
}

위 한 줄에 부울 조합 · 필드 지정 · 부정 이 다 들어가요. 잘 쓰면 Kibana Discover 검색창 처럼 전문가 사용자가 임의 쿼리를 즉석에서 짜는 자리 에 강력.

문제는 문법 위반 시 ES 가 400 에러 를 던져요. 사용자가 그냥 자연어로 쳐 넣은 검색어 에 우연히 : · / · * · ( 같은 글자가 끼면 즉시 파싱 에러 로 검색이 통째로 죽어요. 그래서 사용자 입력을 직접 query_string 에 꽂는 건 절대 금지 가 표준 원칙이에요. 사고 7번에서 다시.

이 약점을 보완한 게 simple_query_string 이에요.

GET /products/_search
{
  "query": {
    "simple_query_string": {
      "query": "+무선 +이어폰 -노바디 \"노이즈 캔슬링\"~2",
      "fields": ["title^3", "description"],
      "default_operator": "and"
    }
  }
}

+ (필수) · - (제외) · "..." (어구) · ~N (slop) · * (prefix wildcard) 같은 단순화된 문법만 허용해요. 문법 위반이 있어도 ES 가 무시하고 가능한 부분만 검색 합니다. 즉 사용자 검색창에 노출 가능한 안전한 버전 이 simple_query_string 이에요.

선택 기준 — 전문가/내부 도구 → query_string, 일반 사용자 → simple_query_string + 그것도 안 쓰는 게 더 안전. 사용자 검색은 대체로 match · multi_match 만으로 충분.

특수 케이스 — match_bool_prefix · intervals · common_terms

match_bool_prefixmatch + bool_prefix 의 단일 필드 버전이에요. 입력을 토큰으로 쪼개 마지막 토큰만 prefix 매칭, 나머지는 일반 토큰 매칭, 묶음은 should 로 처리해요. 자동완성· "타이핑 중" 검색 에 잘 어울려요. search_as_you_type 필드 타입의 기본 쿼리.

intervals 는 8.x 에서 match_phrase + slop 을 훨씬 정밀하게 풀어 주는 쿼리예요. "무선 이어폰" 토큰 사이에 '블루투스' 또는 '노이즈캔슬링' 이 정확히 1개 들어간 경우 같은 조건부 인접성 을 표현할 수 있어요.

"query": {
  "intervals": {
    "title": {
      "all_of": {
        "ordered": true,
        "intervals": [
          { "match": { "query": "무선" } },
          { "any_of": {
              "intervals": [
                { "match": { "query": "블루투스" } },
                { "match": { "query": "노이즈캔슬링" } }
              ]
          }},
          { "match": { "query": "이어폰" } }
        ]
      }
    }
  }
}

표현력이 강하지만 문법이 복잡해서 학습 곡선이 있어요. 법률·논문 검색 같은 정밀 어구 검색 자리에서 진가가 나와요.

common_terms 쿼리는 5.x 때 the · a · 그리고 같은 고빈도 stopword 를 영리하게 다루는 자리로 쓰였지만, 7.x 부터 deprecated · 8.x 부터 제거 됐어요. 대안은 match + cutoff_frequency 또는 match + stopword filter 조합. 신규 코드에서는 등장할 일이 없지만, 예전 코드 마이그레이션 중에 만나면 match 로 치환 하면 돼요.

자주 만나는 사고 7가지

사고 1 — index analyzer ≠ search analyzer 어긋남

원인 — 매핑에서 analyzer: nori 로 색인했는데, 검색 시 analyzer: standard 옵션을 줘서 색인 토큰검색 토큰 이 어긋남. "무선이어폰" 이 색인 때는 [무선, 이어폰] 두 토큰인데 검색 때는 [무선이어폰] 한 덩이라 hit 0.

해결search_analyzer 를 따로 지정한 게 아니라면 색인·검색 analyzer 는 기본적으로 동일 한 게 안전해요. 11편(한국어) 에서 search_analyzer 로 동의어 추가 같은 의도된 어긋남 만 허용.

사고 2 — multi_match best_fields vs most_fields 잘못 선택

원인 — 상품 title 검색을 most_fields 로 설정해 description 길이가 짧고 토큰이 분산된 마이너 상품 이 상위에 떠 버려요. 또는 multi-field (title.korean + title.english) 검색을 best_fields 로 해 한쪽 analyzer 점수만 반영돼 결과가 부분적.

해결단일 텍스트 + 여러 자연 필드 (title/description/brand) = best_fields + boost. 동일 텍스트 + 여러 analyzer (multi-field) = most_fields. 여러 필드를 합쳐 한 단위로 (이름·주소) = cross_fields. 이 세 매핑을 머리에 박아 두세요.

사고 3 — query_string 에 사용자 입력 직접 노출

원인 — 사용자가 검색창에 "가격:5만원 이하" 라고 자연어로 쳤는데, 백엔드가 이 문자열을 그대로 query_string 에 꽂아 가격:5만원 이하필드 가격 = 5만원, 자유 쿼리 이하 로 파싱돼 원하지 않은 결과 + 종종 400 에러.

해결사용자 입력 직접 처리는 match · multi_match, 연산자가 필요한 자리는 simple_query_string + 화이트리스트 필드 만 노출. query_string 은 내부 도구·Kibana 에만.

사고 4 — slop 0 으로 어구 검색하면 동의어가 안 잡힘

원인match_phrase 의 기본 slop 이 0 이라 동의어 필터로 늘어난 토큰동일 position 에 들어가 있어도 position 정렬이 어긋나 어구 검색이 부분적으로 실패. 또는 형태소 분석으로 토큰 2개가 1개가 된 자리 에서 hit 누락.

해결 — 한국어 nori 환경에서 match_phrase 를 쓸 때는 slop: 1 또는 slop: 2 를 기본으로 박는 게 안정적이에요. 동의어 필터를 함께 쓰면 slop더 크게 잡아야 의도된 hit 가 나옵니다.

사고 5 — fuzziness AUTO 무분별 사용

원인 — 모든 match 쿼리에 습관적으로 fuzziness: AUTO 를 박아 둠. 작은 인덱스에서는 응답이 빠르지만, 1억 건 인덱스에서 토큰 후보 셋 확장 비용이 폭증해 응답이 10~100배 느려져요.

해결 — fuzziness 는 오타 가능성이 실제로 큰 자리 (검색창의 자유 입력) 에 한정. 상품 카테고리·태그·필터 등 닫힌 어휘 에서는 끄는 게 정답. 또 boolean prefix · phrase prefix 와 결합 X — 둘 다 비용이 비싸기 때문에 폭망 합쳐짐.

사고 6 — match 의 operator 기본값 OR 로 인한 정밀도 폭락

원인"무선 블루투스 이어폰" 으로 검색했는데 무선만 들어간 마우스 · 블루투스만 들어간 스피커 가 다 hit. 사용자는 세 단어 다 들어간 상품 만 기대했는데 결과 페이지가 어수선.

해결 — 의도가 AND 면 명시적으로 operator: and 를 박거나, 부분 일치 허용이 필요하면 minimum_should_match: "75%" 같은 중간 값. 검색창 UX 마다 적정값이 달라요 — 카테고리 안 검색 은 AND, 전체 검색 은 75%~80% 권장.

사고 7 — match_phrase_prefix 의 후보 폭발

원인 — 검색창에 "이" 한 글자만 친 사용자에게 자동완성을 보여주려고 match_phrase_prefix 를 호출. max_expansions: 50 기본값이라도, "이" 로 시작하는 토큰이 수천 개라 점수 계산이 느려져 응답이 200ms → 3s.

해결prefix 가 너무 짧으면 검색을 막거나 (최소 2~3글자), 자동완성은 search_as_you_type 또는 completion suggester 로 전환. match_phrase_prefix 는 부분 검색 용으로 자리를 한정.

운영 권장 패턴 5가지

(1) 사용자 입력은 match·multi_match 가 디폴트. 90% 의 자리는 이 둘로 충분해요. 연산자가 진짜 필요한 자리만 simple_query_string 으로 확장. query_string 은 전문가 도구 자리에만.

(2) multi_match type 은 의도로 골라요. 상품 검색 = best_fields + boost. multi-field analyzer 검색 = most_fields. 사람 이름·주소 검색 = cross_fields. 어구 = phrase. 자동완성 = bool_prefix 또는 phrase_prefix.

(3) operator·minimum_should_match 를 의도적으로 설계해요. 카테고리 안 검색 AND, 전체 검색 75%~80%, 유사 추천 OR + 큰 minimum_should_match. UX 마다 다른 값이 정답.

(4) 한국어 match_phrase 에는 slop 1~2 기본. nori + 동의어 환경에서는 position 어긋남 이 자주 발생. slop 0 은 영어 환경 에 한정.

(5) fuzziness 와 prefix 는 비싼 자리. 의도된 자리에만. 자동완성은 search_as_you_type 또는 completion suggester 로 옮기고, fuzziness 는 오타 가능성 큰 자유 입력 에 한정.

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

  • Full-text 쿼리 = 입력을 analyzer 로 통과시킨 뒤 토큰 매칭. Term-level (14편) 과 대비.
  • 결과 품질의 결정 변수 = 필드 매핑 analyzer. 같은 쿼리도 analyzer 가 다르면 결과 다 다름.
  • match = 단일 필드, or 기본, operator · minimum_should_match · fuzziness 3 옵션.
  • match_phrase = 토큰 순서·인접성, slop 으로 유연성 조정. 한국어는 slop 1~2 기본.
  • match_phrase_prefix = 마지막 토큰 prefix. max_expansions 가 후보 폭발 가드.
  • multi_match type 6종 — best_fields(상품 검색) · most_fields(multi-field analyzer) · cross_fields(이름·주소) · phrase · phrase_prefix · bool_prefix.
  • title^3 · description · brand^2 같은 boost 가 정렬 결정.
  • query_string = Lucene 전체 문법. 파싱 에러로 사용자 입력 노출 금지.
  • simple_query_string = 문법 위반 무시. 사용자 노출 가능한 안전 버전.
  • match_bool_prefix = 마지막 토큰 prefix 매칭, 자동완성 자리.
  • intervals (8.x) = 정밀 인접성·순서·조건부 토큰 표현. 법률·논문 검색.
  • common_terms = 7.x deprecated, 8.x 제거. match + cutoff_frequency 로 치환.
  • 7대 사고 — analyzer 어긋남 · multi_match type 오선택 · query_string 사용자 노출 · slop 0 한국어 폭망 · fuzziness 무분별 · operator OR 정밀도 폭락 · phrase_prefix 후보 폭발.
  • 권장 디폴트 — 사용자 입력은 match·multi_match, 자동완성은 search_as_you_type·suggester, 전문가 자리만 simple_query_string.

시리즈 다른 편

  • 이전 글 = 12편 Search API — Query DSL · Request Body Search · 응답 구조
  • 다음 글 = 14편 Term-level Queries — term · terms · range · exists · prefix · wildcard · regexp
  • 10편 = Analyzer — tokenizer · filter · char_filter · custom analyzer
  • 11편 = Korean Analyzer — Nori · mecab-ko · 사용자 사전
  • 15편 = Compound Queries — bool · dis_max · function_score · constant_score
  • 16편 = Aggregations Metric — avg · sum · stats · cardinality · percentiles
  • 20편 = Suggesters — term · phrase · completion · context
  • 22편 = Vector Search · kNN — dense_vector · approximate kNN · RAG
  • 32편 = Spring Data Elasticsearch — Repository · Template · POJO
  • 38편 = 시리즈 마무리 — 결정 트리 · 체크리스트 · 자격증

한 줄 정리 — Full-text 쿼리 = analyzer 통과 후 토큰 매칭 으로 자연어 검색을 푸는 ES 의 본 영역. match·match_phrase·multi_match 3개로 90% 자리를 잡고, query_string 은 사용자 입력 노출 금지, multi_match type 6종은 의도로 고른다.

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

답글 남기기

error: Content is protected !!