Term-level 쿼리와 Full-text 쿼리 비교

Term-level 쿼리와 Full-text 쿼리 비교

텍스트를 검색할 때 term-level 쿼리와 full-text 쿼리를 모두 사용할 수 있어요. term-level 쿼리는 주로 구조화된 데이터를, full-text 쿼리는 전체 텍스트 검색을 위해 사용해요. 이 글에서는 두 쿼리의 차이점을 살펴봐요.

출처: 문서

본문

텍스트를 검색할 때 term-level 쿼리와 full-text 쿼리를 모두 사용할 수 있어요. term-level 쿼리는 보통 구조화된 데이터를 검색하는 데 쓰이고, full-text 쿼리는 전체 텍스트 검색(full-text search)에 사용돼요. 둘의 가장 큰 차이는 term-level 쿼리가 지정한 용어와 정확히 일치하는 문서를 검색하는 반면, full-text 쿼리는 쿼리 문자열을 분석(analyze)한다는 점이에요. 다음 표는 term-level 쿼리와 full-text 쿼리의 차이를 요약해요.   Term-level queries Full-text queries 설명(Description) | term-level 쿼리는 어떤 문서가 쿼리와 일치하는지 답해요. full-text 쿼리는 문서가 쿼리와 얼마나 잘 일치하는지 답해요. 분석기(Analyzer) | 검색 용어는 분석되지 않아요. 즉 term 쿼리는 검색 용어를 그대로 검색해요. | 검색 용어는 특정 문서 필드가 인덱싱될 때 사용했던 것과 동일한 analyzer로 분석돼요. 즉 검색 용어가 문서 필드와 동일한 분석 과정을 거쳐요. 관련성(Relevance) | term-level 쿼리는 관련성 점수를 기준으로 정렬하지 않고 일치하는 문서를 반환해요. 그래도 관련성 점수는 계산하지만, 반환된 모든 문서에 대해 동일한 점수예요. | full-text 쿼리는 각 일치 항목에 대해 관련성 점수를 계산하고 관련성 내림차순으로 결과를 정렬해요. 사용 사례(Use Case) | 숫자, 날짜, 태그 같은 정확한 값을 일치시키고 관련성으로 정렬할 필요가 없을 때 term-level 쿼리를 사용해요. | 텍스트 필드를 일치시키고 대소문자, 어간(stemming) 변형 같은 요소를 고려한 뒤 관련성으로 정렬하려면 full-text 쿼리를 사용해요.

OpenSearch는 관련성 점수를 계산할 때 BM25 순위 알고리즘을 사용해요. 자세한 내용은 Okapi BM25를 참고하세요.

full-text 쿼리를 써야 할까, term-level 쿼리를 써야 할까?

full-text 쿼리와 term-level 쿼리의 차이를 명확히 하기 위해, 특정 텍스트 구문을 검색하는 다음 두 예제를 살펴봐요. 셰익스피어 전집이 OpenSearch 클러스터에 인덱싱되어 있다고 가정해요.

이 예제에서는 text_entry 필드에서 셰익스피어 전집에서 “To be, or not to be”라는 구문을 검색해 봐요. 먼저 이 검색에 term-level 쿼리를 사용하세요:

GET shakespeare/_search
{
  "query": {
    "term": {
      "text_entry": "To be, or not to be"
    }
  }
}

응답에는 일치 항목이 없으며, hits가 0으로 표시돼요:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

이것은 “To be, or not to be”라는 용어가 역색인(inverted index)에서 문자 그대로 검색되기 때문이에요. 역색인에는 텍스트 필드의 분석된 값만 저장돼요. term-level 쿼리는 분석된 텍스트 필드 검색에 적합하지 않아요. 의외의 결과를 내는 경우가 많기 때문이에요. 텍스트 데이터를 다룰 때는 keyword로 매핑된 필드에서만 term-level 쿼리를 사용하세요. 이제 같은 구문을 full-text 쿼리로 검색해 봐요:

GET shakespeare/_search
{
  "query": {
    "match": {
      "text_entry": "To be, or not to be"
    }
  }
}

“To be, or not to be” 검색 쿼리는 문서의 text_entry 필드와 마찬가지로 분석되고 토큰 배열로 토큰화돼요. full-text 쿼리는 검색 쿼리와 모든 문서의 text_entry 필드 사이에서 토큰의 교집합을 구한 다음 관련성 점수로 결과를 정렬해요:

{
  "took" : 19,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : 17.419369,
    "hits" : [
      {
        "_index" : "shakespeare",
        "_id" : "34229",
        "_score" : 17.419369,
        "_source" : {
          "type" : "line",
          "line_id" : 34230,
          "play_name" : "Hamlet",
          "speech_number" : 19,
          "line_number" : "3.1.64",
          "speaker" : "HAMLET",
          "text_entry" : "To be, or not to be: that is the question:"
        }
      },
      {
        "_index" : "shakespeare",
        "_id" : "109930",
        "_score" : 14.883024,
        "_source" : {
          "type" : "line",
          "line_id" : 109931,
          "play_name" : "A Winters Tale",
          "speech_number" : 23,
          "line_number" : "4.4.153",
          "speaker" : "PERDITA",
          "text_entry" : "Not like a corse; or if, not to be buried,"
        }
      },
      {
        "_index" : "shakespeare",
        "_id" : "103117",
        "_score" : 14.782743,
        "_source" : {
          "type" : "line",
          "line_id" : 103118,
          "play_name" : "Twelfth Night",
          "speech_number" : 53,
          "line_number" : "1.3.95",
          "speaker" : "SIR ANDREW",
          "text_entry" : "will not be seen; or if she be, its four to one"
        }
      }
    ]
  }
}
...

모든 full-text 쿼리 목록은 Full-text queries를 참고하세요.

speaker 필드에서 “HAMLET” 같은 정확한 용어를 검색하고 관련성 점수로 정렬할 필요가 없다면 term-level 쿼리가 더 효율적이에요:

GET shakespeare/_search
{
  "query": {
    "term": {
      "speaker": "HAMLET"
    }
  }
}

응답에는 문서 일치 항목이 포함돼요:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1582,
      "relation" : "eq"
    },
    "max_score" : 4.2540946,
    "hits" : [
      {
        "_index" : "shakespeare",
        "_id" : "32700",
        "_score" : 4.2540946,
        "_source" : {
          "type" : "line",
          "line_id" : 32701,
          "play_name" : "Hamlet",
          "speech_number" : 9,
          "line_number" : "1.2.66",
          "speaker" : "HAMLET",
          "text_entry" : "[Aside]  A little more than kin, and less than kind."
        }
      },
      {
        "_index" : "shakespeare",
        "_id" : "32702",
        "_score" : 4.2540946,
        "_source" : {
          "type" : "line",
          "line_id" : 32703,
          "play_name" : "Hamlet",
          "speech_number" : 11,
          "line_number" : "1.2.68",
          "speaker" : "HAMLET",
          "text_entry" : "Not so, my lord; I am too much i' the sun."
        }
      },
      {
        "_index" : "shakespeare",
        "_id" : "32709",
        "_score" : 4.2540946,
        "_source" : {
          "type" : "line",
          "line_id" : 32710,
          "play_name" : "Hamlet",
          "speech_number" : 13,
          "line_number" : "1.2.75",
          "speaker" : "HAMLET",
          "text_entry" : "Ay, madam, it is common."
        }
      }
    ]
  }
}
...

term-level 쿼리는 정확한 일치를 제공해요. 따라서 “Hamlet”으로 검색하면 일치 항목이 나오지 않아요. “HAMLET”은 keyword 필드이므로 분석된 형태가 아니라 OpenSearch에 문자 그대로 저장되기 때문이에요. 검색 쿼리 “HAMLET”도 문자 그대로 검색돼요. 이 필드에서 일치 항목을 얻으려면 정확히 같은 문자를 입력해야 해요.

Should I use a full-text or a term-level queryExample: Phrase search

더 알아보기 (Learn more)