연산자

여기서는 쿼리에서 사용 가능한 연산자를 설명합니다. 사용 가능한 연산자는 다음과 같습니다.

비교: $lt, $le, $gt, $ge, $eq, $ne

비교 연산자는 항목간 비교하거나 같거나 다른지를 판단합니다.

다음 예시는 key1 항목이 10보다 작은지를 검사합니다.

"key1": {
    "$lt": 10
}

포함: $in, $not_in

$in$not_in은 필드에 특정한 값들이 포함되는지 포함되지 않는지를 검사하는 연산자입니다.

다음 예시는 key1 항목에 1, 2, 5 중에 하나가 포함되어 있는지 검사합니다.

"key1": {
    "$in": [1, 2, 5]
}

존재 여부: $exists

필드에 값이 존재하는지 확인합니다.

다음 예시는 key1에 값이 존재 여부를 확인합니다.

"key1": {
    "$exists": True
}

부분 일치: $match

와일드카드(wildcard)를 사용하여 일치 여부를 확인합니다.

다음 예시는 key1search로 시작하는 문자열이 있는지 확인합니다. $optionsignore-case를 전달하면 대소문자 구별없이 확인할 수 있습니다.

"key1": {
    "$match": "search*",
    "$options": "ignore-case"
}

정규식 일치: $regex

정규식 (opens in a new tab)을 사용하여 일치 여부를 확인합니다.

다음은 key1에 숫자가 포함된 문자열을 찾는 예시입니다.

"key1": {
    "$regex": ".+\d+",
}

Boolean: $and, $or, $nor, $not

불리언 연산자를 통해 조건을 조합하여 검색할 수 있습니다.

아래는 id가 1, 2, 5 중에 하나이면서 titlesearch가 일치하지 않는 데이터를 찾는 쿼리의 예시입니다.

"$and": [
    {
        "id": {
            "$in": [1, 2, 5]
        }
    },
    {
        "$not": [
            {
                "title": {
                    "$match": "search*"
                }
            }
        ]
    }
]

Project: $project

$project는 필드를 선택하는 연산자입니다.

다음 예시는 key1, key2를 선택하는 예시입니다.

"$project": ["key1", "key2"]

필요에 따라 다음과 같이 별명을 지정할 수 있습니다. 아래의 예시에서 key1key3로 출력됩니다.

"$project": [
    {"key1": "key3"},
    "key2"
]

Nested field의 경우에는 다음과 같이 접근할 수 있습니다. 이 예시는 attribute의 계층 구조를 유지하면서 attribute의 내부 필드만 선택하게 됩니다. 따라서 출력되는 칼럼은 attribute 하나입니다.

"$project": ["attribute.author", "attribute.price"]

하지만 다음과 같이 ^ 연산자를 추가하면 계층 구조가 해제되면서 attribute.authorattribute.price 두 개의 칼럼으로 출력되게 됩니다.

"$project": ["^attribute.author", "^attribute.price"]

갯수 제한: $limit

$limit는 연산된 결과값에서 선택할 행의 수를 제한하는 연산자입니다.

다음 예시는 10개의 행을 선택하는 예시입니다.

"$limit": 10

시작 위치: $skip

$skip는 연산된 결과값에서 시작할 위치를 제한하는 연산자입니다.

다음 예시는 5번째 행부터 선택하는 예시입니다.

"$skip": 5

$skip$limit를 사용하여 pagination을 구현할 수 있습니다.

정렬: $sort

$sort는 연산된 결과값을 정렬하는 연산자입니다.

다음 예시는 key1은 오름차순, key2를 내림차순으로 결과값을 정렬하는 예시입니다. 나열된 순서를 우선하여 정렬됩니다.

"$sort": [
    "key1",
    {"key2": "desc"},
]

Flatten: $unnest, $recursive_unnest

$unnest는 지정된 칼럼의 데이터를 1차원 배열로 변환하는 연산자입니다.

예를 들어 다음과 같이 데이터가 있고

{
    "title": "Hello Aeca!",
    "attributes": {
        "author": "finn",
        "publisher": {
            "name": "Aeca, Inc."
        }
    },
}

이 데이터를 다음과 같이 쿼리하면

"$unnest": "attributes"

다음과 같이 출력됩니다.

[
    "title": "Hello Aeca!",
    "attributes.author": "finn",
    "attributes.publisher": {
        "name": "Aeca, Inc."
    }
]

$unnest는 위의 예시처럼 첫번째 깊이의 해당되는 필드만 변환하기 때문에 publisher처럼 두단계 깊이 이상의 필드를 확장하고자 한다면 $recursive_unnest를 사용할 수 있습니다.

"$recursive_unnest": "attributes"

위 쿼리는 다음과 같이 출력됩니다.

[
    "title": "Hello Aeca!",
    "attributes.author": "finn",
    "attributes.publisher.name": "Aeca, Inc."
]

값 지정: $literal

특정한 값을 칼럼에 추가합니다.

다음과 같은 데이터가 있을 때

data = [
    {"doc_id": 1, "data": 1},
    {"doc_id": 2, "data": 2},
    {"doc_id": 3, "data": 3},
]

다음과 같이 실행하면

query = {
    "$literal": {
        "text": "test"
    }
}

다음과 같은 결과를 얻을 수 있습니다.

실행 결과
   doc_id  data  text
0       1     1  test
1       2     2  test
2       3     3  test

통합: $group

$group 쿼리된 결과값을 통합하여 재가공하는 연산자입니다.

여기서는 검색에서 사용하는 예시를 다시 사용하겠습니다. 우리는 6기통인 모델의 연도별 모델 수와 모델명, 해당연도 마력의 최대값을 구해보고자 합니다. 이 쿼리는 다음과 같이 작성할 수 있습니다.

query = [
    {
        "cylinders": 6,
    },
    {
        "$group": {
            "_id": "model_year",
            "first_name": {
                "$first": "name"
            },
            "horsepower_max": {
                "$max": "horsepower"                
            },
            "count": {
                "$sum": 1
            }
        }
    }
]
df = doc_db.find(collection_name, query)
print(df)

하나씩 살펴보면, $group은 병합의 기준이 되는 칼럼인 _id를 필수적으로 입력 받습니다. 이 경우 연도별 병합이기 때문에 model_year를 선택했습니다.

"_id": "model_year"

다음 병합된 그룹에서 첫번째 모델의 name을 가져옵니다. 여기서 first_name$group으로 만들어질 결과물 칼럼의 이름입니다.

"first_name": {
    "$first": "name"
}

마지막 항목인 병합 결과물에 count라는 칼럼을 신규로 만들고 "$sum": 1 하여 model_year로 병합된 아이템들의 갯수를 계산합니다.

"count": {
    "$sum": 1
}

실행 결과는 다음과 같습니다.

실행 결과
    _id  count               first_name  horsepower_max
0    70      4          plymouth duster            97.0
1    71      8              amc gremlin           110.0
2    73      8         plymouth valiant           122.0
3    74      7          plymouth duster           110.0
4    75     12  plymouth valiant custom           110.0
5    76     10         plymouth valiant           120.0
6    77      5       chevrolet concours           110.0
7    78     12       pontiac phoenix lj           165.0
8    79      6        pontiac lemans v6           115.0
9    80      2              dodge aspen           132.0
10   81      7       chevrolet citation           120.0
11   82      3    buick century limited           112.0

$group의 연산자

병합에 사용할 수 있는 연산자는 다음과 같습니다.

Join: $join, $union

두개의 컬렉션을 병합합니다.

test.join.lefttest.join.right를 생성하고 예제 데이터를 삽입하도록 하겠습니다.

from aeca import DocumentDB
 
doc_db = DocumentDB(channel)
 
collection_names = ["test.join.left", "test.join.right"]
indexes = [
    {
        "index_type": "kPrimaryKey",
        "fields": ["doc_id"]
    }
]
data = {
    "test.join.left": [
        {"doc_id": 1, "left": 1},
        {"doc_id": 2, "left": 2},
        {"doc_id": 3, "left": 3},
    ],
    "test.join.right": [
        {"doc_id": 1, "right": 1},
        {"doc_id": 2, "right": 2},
        {"doc_id": 5, "right": 5},
    ]
}
 
for name in collection_names:
    doc_db.create_collection(name, indexes=indexes)
    doc_db.insert(name, data[name])

$join

다음은 test.join.lefttest.join.rightinner join 하는 예시입니다.

query = {
    "$join": {
        "type": "inner",
        "collection": "test.join.left",
        "query": {},
        "on": ["doc_id"]
    }
}
df = doc_db.find("test.join.right", query)
print(df)

$join에서 사용 가능한 옵션은 다음과 같습니다.

  • type: 조인 방법
    • inner: 내부 조인, 기본값
    • outer: 외부 조인
    • left: left 조인
    • right: right 조인
    • hash_inner: 해시 내부 조인
  • collection: 조인할 컬렉션
  • query: 조인할 컬렉션의 쿼리, 생략 가능
  • on: 조인할 키

위 예시의 실행 결과는 다음과 같습니다.

실행 결과
   doc_id  left  right
0       1     1      1
1       2     2      2

$union

다음은 test.join.lefttest.join.right$union 하는 예시입니다.

query = {
    "$union": {
        "type": "all",
        "collection": "test.join.left",
        "query": {},
        "on": ["doc_id"]
    }
}
df = doc_db.find("test.join.right", query)
print(df)

$join에서 사용 가능한 옵션은 다음과 같습니다.

  • type: 조인 방법
    • all: 전체
    • distinct: 중복 제거
  • collection: 조인할 컬렉션
  • query: 조인할 컬렉션의 쿼리, 생략 가능
  • on: 조인할 키

위 예시의 실행 결과는 다음과 같습니다.

실행 결과
   doc_id  left  right
0       1  <NA>      1
1       2  <NA>      2
2       5  <NA>      5
3       1     1   <NA>
4       2     2   <NA>
5       3     3   <NA>

색인 힌트: $hint

색인을 위한 힌트를 제공합니다. 컬렉션에는 하나 이상의 색인이 저장될 수 있고 입력된 쿼리에 대해 쿼리 플래너는 이 색인들을 조합하여 최적의 색인을 선택합니다. 그러나 $hint를 통해 색인이 선택되면 해당 색인을 우선적으로 사용합니다.

{
    "$search": {
        "query": "cat"
    },
    "$hint": "sk_fts"
}

전문 검색: $search

$search전문 검색하는 연산자입니다.

다음은 검색 예시입니다.

"$search": {
    "query": "content:(hello world)",
    "fields": ["content"]
}

사용 가능한 옵션은 다음과 같습니다.

항목설명타입기본값
query검색 쿼리
쿼리 작성 방법은 전문 검색를 참고
str
fields검색 대상 필드list[str]색인된 전체 필드
skip시작 위치int
limit후보수int
timeout만료 시간int
min_score최소 점수float
highlight하이라이트bool | dict
default_operator+: Must
#: Filter
strNone: Should
script_score스크립트dict
custom_stop_words불용어list[str]
custom_synonyms유의어list[dict] | dict

highlight

highlight가 활성화되면 검색 결과물에 _highlights 칼럼이 추가되고 각 필드에 <em> 태그로 강조된 문장이 반환됩니다.

만약 이 태그를 변경하고자 한다면 다음과 같이 설정할 수 있습니다.

{
    "$search": {
        "query": "Hello Aeca",
        "highlight": {
            "enabled": True,
            "pre_tag": "<b>",
            "post_tag": "</b>"
        }
    }
}

script_score

script_score는 Aeca에 내장된 Python 인터프리터를 이용하여 $search를 통해 계산된 문서의 점수를 재계산할 수 있습니다. 여기서 내부 변수는 다음과 같이 미리 할당되어 있습니다.

  • _doc: 필드 전체 값을 가지고 있는 변수
  • _score: $search를 통해 계산된 기존 점수

Python으로 작성되는 아래의 코드 script_score.script.sourcequery를 통해 검색된 결과물의 기존 점수인 _score_doc["Released_Year"]에서 연도값만 추출하여 더하는 코드로 구성되어 있습니다. 결과적으로 최신순 정렬와 유사한 동작을 하게 됩니다.

"$search": {
    "script_score": {
        "query": "x-men",
        "script": {
            "source": "_score + datetime.datetime.strptime(_doc['Released_Year'], '%Y-%m-%dT%H:%M:%S').year"
        },
    }
}

스크립트에서 사용 가능한 라이브러리는 Python의 builtin 라이브러리와 chrono, datetime, math, numpy입니다.

custom_synonyms

custom_synonyms를 사용하여 검색시 유의어를 사용할 수 있습니다. 예를 들어 "애플"을 검색했을 때 "Apple"도 함께 검색 결과를 확인하고 싶다면 "애플"의 유의어로 "Apple"을 추가하는 방법을 생각할 수 있습니다.

다음은 사용 예시입니다. 여기서 hello["hello", "hi", "안녕"]로 확장됩니다. 혹은 "cognica": ["aeca", "에이카"]와 같이 정의하여 특정 키워드로 교체할 수도 있습니다.

{
  "$search": {
    "query": "Hello, Aeca!",
    "custom_synonyms": [
      {
        "hello": ["hello", "hi", "안녕"],
        "aeca": ["aeca", "cognica", "에이카", "코그니카"]
      },
      {
        // "cognica"는 "aeca"와 "에이카"로 교체됩니다.
        "cognica": ["aeca", "에이카"]
      }
    ]
  }
}

만약 특정 필드별로 유의어를 설정하고자 한다면 다음과 같이 정의할 수 있습니다.

{
  "$search": {
    "query": "Hello, Aeca!",
    "custom_synonyms": {
      "title": [
        {
          "hello": ["hello", "hi", "안녕"],
          "aeca": ["aeca", "cognica", "에이카", "코그니카"]
        },
        {
          "cognica": ["aeca", "에이카"]
        }
      ],
      "content": [
        {
          "hello": ["hello", "hi", "안녕"],
          "aeca": ["aeca", "cognica", "에이카", "코그니카"]
        },
        {
          "cognica": ["aeca", "에이카"]
        }
      ]
    }
  }
}

스크립트: $script

스크립트는 Aeca에 내장된 Python 인터프리터를 이용하여 필터 조건을 계산할 수 있습니다. 여기서 내부 변수는 다음과 같이 미리 할당되어 있습니다.

  • _doc: 필드 전체 값을 가지고 있는 변수
  • _value: 선택된 필드의 값

만약 다음과 같이 데이터를 입력하고

data = [
    {"doc_id": 1, "data": ["A", "C"]},
    {"doc_id": 2, "data": ["A", "B", "D"]},
    {"doc_id": 3, "data": ["A"]},
]
doc_db.insert(name, data)

다음과 같이 쿼리하면 길이가 data의 배열의 길이가 2인 doc_id가 1인 문서만 반환됩니다.

여기에 작성되는 스크립트는 Python으로 작성되며 내부 변수인 _doc는 행 전체의 데이터를 가지고 있으며 dict 타입처럼 필드의 값에 접근할 수 있습니다. 아래의 경우 data 필드가 선택되어 있으므로 이 경우 내부 변수인 _value_doc["data"]와 동일한 값을 가지고 있습니다. 따라서 아래의 쿼리는 len(_value) == 2와 동일합니다.

{
    "data": {
        "$script": 'len(_doc["data"]) == 2'
    }
}

스크립트에서 사용 가능한 라이브러리는 Python의 builtin 라이브러리와 chrono, datetime, math, numpy입니다.