전문 검색(Full-Text Search)

여기서는 전문 검색 쿼리를 작성하는 방법을 설명합니다. 전문 검색은 AND, OR 같은 연산자를 조합하거나 doc_id:10과 같이 특정한 조건에 부합하는 단어(혹은 토큰)의 일치 여부를 점수화하여 종합적으로 가장 연관된 문서를 찾는 방법입니다.

전문 검색은 다음 예시처럼 여러 연산자 중 하나인 $search를 사용합니다. 이어서 $project와 같이 다른 연산자와 함께 사용하는 것이 가능합니다.

{
    "$search": {
        "query": f"(content:({query}))^0.2 AND (embedding:[{query_embed}])^20",
    },
    "$project": [
        "doc_id",
        "content",
        {"_meta": "$unnest"}
    ],
    "$hint": "sk_fts"
}

전문 검색을 위해서는 색인이 준비되어 있어야 합니다. 색인의 구성에 따라 검색 결과도 달라집니다.

문법

여기서는 $search의 인자인 query의 작성 방법에 대해서 설명합니다. 벡터 검색을 제외하고 입력된 문서와 쿼리는 Analyzer에 의해 토큰으로 분해되고 문서와 쿼리간의 토큰의 일치 여부를 계산하게 됩니다.

기본적으로 아래와 같이 간단히 쿼리만 전달할 수 있습니다. 이 경우 일반적으로 ApplePineapple으로 분리된 단어가 일치하는 문서들을 찾게 됩니다.

Apple Pineapple

만약 하나 이상이 단어가 일치하는 문서를 찾고자 한다면 다음과 같이 "로 묶어 검색할 수 있습니다.

"white cat"

여기서는 $search 연산자에 설정된 기본값에 따라서 동작하게 됩니다. 기본 동작은 다음과 같습니다.

  • 전문 검색으로 설정된 모든 필드가 대상이 됨
  • 기본 연산은 or(should)

Boolean

Boolean 연산자를 통해 Term 검색 조건을 제어할 수 있습니다.

다음 쿼리는 China 출현하면서 USUSD가 모두 출현하지 않는 문서를 검색하고 있습니다.

(China AND -(US OR USD))

지원하는 연산자는 다음과 같습니다.

  • AND: and
  • OR: or
  • NOT: not
  • +: must
  • -: must not

필터 검색

필터 연산은 # 연산자를 통해 사용할 수 있습니다. 필터 연산은 점수 계산에 관여되지 않고 조건만 만족하는지 판단합니다. 따라서 필터 연산으로만 쿼리가 입력된다면 점수는 기본값만 출력됩니다.

cats AND #(cat_type:balinese AND weight:>8.0)

필터 연산은 점수 기반의 연산인 must(+), should, must not(-)과 함께 사용하며 검색 집합의 크기(cardinality)를 줄여 검색 속도 개선에 활용할 수 있습니다.

그룹화

그룹화를 통해 쿼리 연산의 우선순위를 조절할 수 있습니다.

다음 예시처럼 공백이 포함된 natural languagetitle 필드에 한정하여 검색하게 하거나 AI AND "machine learning"의 subquery에 대해 boosting 연산을 추가할 때 활용할 수 있습니다.

section:economy title:(natural language) content:(AI AND "machine learning")^2

필드 검색

필드 검색을 사용하여 특정한 필드만 검색할 수 있습니다.

다음 쿼리는category 필드에서 news이면서 authorted인 문서를 찾고 있습니다.

category:news AND author:ted

다음 쿼리는 title 필드에서 China 또는 US가 출현하고 전체 필드에서 Korea가 출현한 문서를 찾고 있습니다.

title:(China OR US) AND Korea

만약 복수의 필드에 동일한 값을 지정하고 싶다면 다음과 같이 입력할 수 있습니다.

{!fields=[title, content]}:(China AND US)

이 쿼리는 title:(China AND US) content:(China AND US)로 해석됩니다.

와일드카드(Wildcard) 검색

와일드카드를 사용하여 단어의 일부가 일치하는 문서를 검색할 수 있습니다.

lake* *like

퍼지(Fuzzy) 검색

퍼지 검색은 특정한 편집 거리 (opens in a new tab) 이하의 차이를 허용하는 검색 방식입니다.

예를 들어 applapple 편집 거리가 1이고 다음과 같이 쿼리하면 검색 과정에서 일치하는 단어로 다룰 수 있습니다. ~ 다음에 출현하는 숫자는 편집 거리를 의미합니다.

appl~1

근접 검색

하나 이상의 단어로 구성된 쿼리의 단어가 완전히 일치하지 않더라도 단어 단위로 일정 편집 거리 (opens in a new tab) 이하로 근접했을 때 이를 허용하고자 한다면 근접 검색을 사용할 수 있습니다.

예를 들어 문서에 funny baby cat이란 문장이 존재하고 다음과 같이 쿼리하면 이 문서는 일치하게 됩니다. ~ 다음에 출현하는 숫자는 편집 거리를 의미합니다.

"funny cats"~2

추가적으로 단어의 출현 순서를 고려하지 않는 근접 검색은 ~~ 연산자를 통해 검색할 수 있습니다.

"funny cats"~~2

벡터 검색

벡터 검색은 임베딩된 벡터를 사용하여 벡터간 유사도를 기반하여 검색하는 방법입니다. 벡터 검색을 위해서는 다음과 같이 encoder를 통해 임베딩된 벡터를 생성하고 실수 타입의 배열 스트링으로 변환하여 쿼리할 수 있습니다.

query = "그 여자는 달걀 하나를 그릇에 깨어 넣었다."
query_embedding = encoder.encode(query)
query_embed = ", ".join([str(e) for e in query_embedding[0]])
 
search_query = {
    "$search": {
        "query": f"embedding:[{query_embed}]"
    },
    "$hint": "sk_fts",
}

위 예시에서 embedding 필드는 벡터 검색을 위한 색인으로 준비되어 있어야 합니다.

Boosting

전문 검색에서 출력하는 문서의 순위는 내부적으로 계산되는 점수에 의존합니다. 이 점수는 토큰의 일치 여부, 근접 등 여러 요인으로 결정되고 필요에 따라 특정한 단어의 일치 여부를 중요하게 다루고 싶을 수 있습니다. 이 경우 Boosting을 사용하여 계산 되는 점수의 가중치를 조절할 수 있습니다.

다음의 예시는 term 기반의 검색과 벡터 검색을 통합한 경우이며 term 기반의 검색에 대해서 (content:(그 여자는 달걀 하나를 그릇에 깨어 넣었다))^0.2에서와 같이 0.2의 가중치를 부여하고 벡터 기반의 검색에 대해 (embedding:[0.12, 0.92, ... , 0.10])^2020의 가중치를 부여하는 예시입니다.

(content:(달걀 그릇에 깨어 넣었다))^0.2 AND (embedding:[0.12, 0.92, ... , 0.10])^20

비교 / 범위

특정 필드가 Int64AnalyzerFloat64Analyzer와 같은 비교 가능한 타입의 색인이 지정되어 있다면 다음과 같이 비교 연산자를 사용하여 조건에 맞는 특정값을 검색할 수 있습니다.

다음 예시는 age가 10을 초과하는 값을 검색합니다.

age:>10

혹은 다음과 같이 범위 연산자를 사용하여 특정 범위의 값을 찾을 수 있습니다. 다음은 1보다 큰 값을 찾는 예시이며 여기서 *는 모든 값을 의미합니다.

age:[1 TO *]

활용

문서 수

FTS로 검색하는 경우 내부 변수인 cursor.fts.total_hits에는 전체 문서수를 가지고 있습니다. 이를 활용하여 다음과 같이 쿼리하며 total_hits라는 칼럼이 추가되고 여기에는 전체 문서 수가 저장됩니다.

"$literal": {
    "total_hits": {
        "$prop": "cursor.fts.total_hits"
    }
}