1. 기본 세팅

엘라스틱 서치 설치 : https://www.elastic.co/kr/downloads/elasticsearch

한글 형태소 분석기 노리 설치: https://www.elastic.co/kr/blog/nori-the-official-elasticsearch-plugin-for-korean-language-analysis

파이썬3 엘라스틱서치 패키지 설치: https://elasticsearch-py.readthedocs.io/en/master/

2. 파이썬 Elastic Search 세팅

아래는 위에서 설치 받은 한글 형태소 분석기인 nori를 특정 필드에 적용도록 하는 세팅 방법 입니다.

from elasticsearch import Elasticsearch

# es 실행 기본포트: 9200, 기본 ip: 루프백 아이피 - 127.0.0.1 (localhost)
es = Elasticsearch()

# test-index 인덱스가 이미 사용하고 있을 시, 삭제
es.indices.delete(index='test-index', ignore=[400, 404])
body = {
    "settings": {
        "index": {
            "analysis": {
				
                "tokenizer": {
                    "nori_tokenizer": {
                        "type": "nori_tokenizer",
                    },
                },
                "analyzer": {
					# nori 분석기 설정
                    "nori_korean": {
                        "type": "custom",
                        "tokenizer": "nori_tokenizer"
                    },
                }
            }
        }
    },
    "mappings": {
        "goods": {
            "properties": {
                "name": {
                    "type": "text",
					# name에 nori 형태소 분석기 설정
                    "analyzer": "nori_korean",
                },
                "description": {
                    "type": "text",
                },
                "name_eng": {
                    "type": "text"
                },
                "pid": {
                    "type": "integer"
                },
            }
        }
    }
}
es.indices.create(index='test-index', body=body)
# 테스트 데이터
goods = [
    {
        "name": '주름원피스',
        "name_eng": "Pleated dress",
        "pid": 0,
        "description": '주름진 원피스'
    }
]
for i, data in enumerate(goods):
    res = es.index(index="test-index", doc_type='goods', body=data, id=i)
    print(res['result'])


3. 검색하기

query = {
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "name": {
                            "query": "주름", "boost": 3
                        }
                    }
                }
            ]
        }
    }
}

res = es.search(index="test-index", body=query)


Django 모델에서 Manager는 데이터베이스와 상호 작용하는 인터페이스입니다. 기본적으로 Manager는 Model.objects 속성을 통해 사용할 수 있습니다. Django 모델마다 기본적으로 사용되는 기본 관리자는 django.db.models.Manager입니다.

from django.db import models

class DocumentManager(models.Manager):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentManager()


위 예시에서 filter_type='pdf'를 선언하는 방법은 아래와 같습니다. 또한 호출 후, filter()와 order_by()같이 추가적으로 연결하여 사용할 수 있습니다.

Document.objects.pdfs()


Document.objects.pdfs().filter(name='test')
Document.objects.pdfs().order_by('name')


하지만 Manager에 선언한 다른 메소드를 호출하고자 하면 오류가 발생합니다.

Document.objects.pdfs().smaller_than(1000)


# AttributeError: 'QuerySet' object has no attribute 'smaller_than'


이 문제를 해결하는 것은 Custom QuerySet을 선언하는 것입니다.

class DocumentQuerySet(models.QuerySet):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class DocumentManager(models.Manager):
    def get_queryset(self):
        return DocumentQuerySet(self.model, using=self._db)  # 중요

    def pdfs(self):
        return self.get_queryset().pdfs()

    def smaller_than(self, size):
        return self.get_queryset().smaller_than(size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentManager()




Document.objects.pdfs().smaller_than(1000).exclude(name='Article').order_by('name')


만약 Custom QuerySet만 정의하고자 하면 아래와 같이 선언하면 됩니다.

class DocumentQuerySet(models.QuerySet):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentQuerySet.as_manager()


pdfs()만 호출할 수 있는 것은 물론, smaller_than() 또한 연결하여 호출할 수 있습니다.

Document.objects.pdfs().smaller_than(1000)


models.py 안에 조회 쿼리를 넣을 수 있지만 코드가 커진다면 Manager와 QuerySet을 managers.py라는 다른 모듈에 유지하는 것을 추천합니다. Custom Manager와 Custom QuerySet을 사용하는 이점은 공통적으로 사용되는 쿼리를 공통 함수로 정의할 수 있고 실제 동작을 숨길 수 있습니다.

ORM을 조인하고 싶을 때 N:1의 관계나 N:N의 관계일 경우, prefetch_related()를 사용하게 됩니다. 

모델 정의

예시를 들기 위해 아래와 같이 모델과 모델간 관계를 정의합니다.

from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)
	vegetarian = models.BooleanField()

    def __str__(self):
        return "%s (%s)" % (
            self.name,
            ", ".join(topping.name for topping in self.toppings.all()),
        )


class Restaurant(models.Model):
    pizzas = models.ManyToManyField(Pizza, related_name='restaurants')
    best_pizza = models.ForeignKey(Pizza, related_name='championed_by', on_delete=models.CASCADE)

Prefetch

만약 첫 번째 레스토랑에서 채식주의자가 먹을 수 있는 피자를 조회하는 쿼리는 아래와 같습니다.

queryset = Pizza.objects.filter(vegetarian=True)


restaurants = Restaurant.objects.prefetch_related(Prefetch('pizzas', queryset=queryset))
vegetarian_pizzas = restaurants[0].pizzas.all()


만약 위와 같은 queryset이 다른 조인 쿼리에도 사용된다면 해당 쿼리가 실행될 때마다 새로 조회를 하므로 중복조회가 발생됩니다. 이때, Prefetch()에서 제공하는 to_attr을 사용하여 쿼리를 메모리에 저장하여 효율적으로 사용할 수 있습니다.

queryset = Pizza.objects.filter(vegetarian=True)


restaurants = Restaurant.objects.prefetch_related(Prefetch('pizzas', queryset=queryset, to_attr='vegetarian_pizzas'))
vegetarian_pizzas = restaurants[0].vegetarian_pizzas


to_attr에 저장되는 Prefetch()의 데이터 크기가 너무 크지 않다면, 메모리에 올려 재사용성을 늘리는 것이 효율적입니다.

문자열로 되어 있는 시간을 Datetime 객체로 변경하는 방법은 많이 찾아 볼 수 있습니다. 여기서는 기존에 존재하는 방법과 추가 설치 모듈로 좀 더 간편하게 사용하는 방법을 설명하겠습니다.

1. Datetime

파이썬에 내장되어 있는 Datetime 패키지를 사용하여 아래와 같이 변환을 할 수 있습니다.

import datetime

date_time_str = '2018-06-29 08:15:27.243860'  
date_time_obj = datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S.%f')

print('Date-time:', date_time_obj)


# Date-time: 2018-06-29 08:15:27.243860


흔히 사용하는 포맷은 ISO 8601으로  YYYY-MM-DDTHH:MM:SS.mmmmmm 와 같이 표현하고 있습니다. 이와 같은 포맷만 들어온다면 그대로 사용해도 무방하지만 아래와 같이 여러 타입이 존재한다면 고려할 사항이 많아집니다.

"Jun 28 2018 at 7:40AM" -> "%b %d %Y at %I:%M%p"
"September 18, 2017, 22:19:55" -> "%B %d, %Y, %H:%M:%S"
"Sun,05/12/99,12:30PM" -> "%a,%d/%m/%y,%I:%M%p"
"Mon, 21 March, 2015" -> "%a, %d %B, %Y"
"2018-03-12T10:12:45Z" -> "%Y-%m-%dT%H:%M:%SZ"

timezone 지정

위처럼 문자열을 datetime 객체로 변환하거나 현재 날짜+시간을 호출하면 현재 로컬시간을 기준으로 호출이 됩니다.

now = datetime.datetime.now()
print(now)


# 2019-02-02 15:54:07.244639


여기서 구한 현재 시간을 뉴욕시간대로 변경하고자 하면 아래와 같이 복잡한 절차를 거치게 됩니다. 또한 현재 시간이 뉴욕시간대로 변환되지 않고 timezone만 추가된 형태를 확인할 수 있습니다.

import datetime as dt
import pytz

date_time_obj = dt.datetime.now()

print('now:', date_time_obj)

timezone = pytz.timezone('America/New_York')
timezone_date_time_obj = timezone.localize(date_time_obj)

print(timezone_date_time_obj)
print(timezone_date_time_obj.tzinfo)


# now: 2019-02-02 15:56:15.197103
# 2019-02-02 15:56:15.197103-05:00
# America/New_York


datetime 패키지로만 충분히 변환하거나 시간대를 바꿀 수 있지만 다른 모듈을 설치하면 좀 더 간편하게 사용할 수 있습니다.

2. dateutil

dateutil 모듈을 설치하여 사용하면 문자열의 시간 포맷을 걱정할 필요가 없습니다.

from dateutil.parser import parse

date_array = [
    '2018-06-29 08:15:27.243860',
    'Jun 28 2018  7:40AM',
    'Jun 28 2018 at 7:40AM',
    'September 18, 2017, 22:19:55',
    'Sun, 05/12/1999, 12:30PM',
    'Mon, 21 March, 2015',
    '2018-03-12T10:12:45Z',
    '2018-06-29 17:08:00.586525+00:00',
    '2018-06-29 17:08:00.586525+05:00',
    'Tuesday , 6th September, 2017 at 4:30pm'
]

for date in date_array:
    print('Parsing: ' + date)
    dt = parse(date)
    print(dt.date())
    print(dt.time())
    print(dt.tzinfo)
    print()



# Parsing: 2018-06-29 08:15:27.243860
# 2018-06-29
# 08:15:27.243860
# None

# Parsing: Jun 28 2018  7:40AM
# 2018-06-28
# 07:40:00
# None

# Parsing: Jun 28 2018 at 7:40AM
# 2018-06-28
# 07:40:00
# None

# Parsing: September 18, 2017, 22:19:55
# 2017-09-18
# 22:19:55
# None

# Parsing: Sun, 05/12/1999, 12:30PM
# 1999-05-12
# 12:30:00
# None

# Parsing: Mon, 21 March, 2015
# 2015-03-21
# 00:00:00
# None

# Parsing: 2018-03-12T10:12:45Z
# 2018-03-12
# 10:12:45
# tzutc()

# Parsing: 2018-06-29 17:08:00.586525+00:00
# 2018-06-29
# 17:08:00.586525
# tzutc()

# Parsing: 2018-06-29 17:08:00.586525+05:00
# 2018-06-29
# 17:08:00.586525
# tzoffset(None, 18000)

# Parsing: Tuesday , 6th September, 2017 at 4:30pm
# 2017-09-06
# 16:30:00
# None


위 예시처럼 시간포맷을 알 필요가 없다는 장점이 있지만 timezone을 지정하지 않으면 로컬 시간을 바라보고 있는 것을 확인할 수 있습니다.

3. maya

마야 또한 위의 dateutil 모듈과 같이 시간 포맷에 상관없이 변환할 수 있습니다.

import maya

date_array = [  
    '2018-06-29 08:15:27.243860',
    'Jun 28 2018  7:40AM',
    'Jun 28 2018 at 7:40AM',
    'September 18, 2017, 22:19:55',
    'Sun, 05/12/1999, 12:30PM',
    'Mon, 21 March, 2015',
    '2018-03-12T10:12:45Z',
    '2018-06-29 17:08:00.586525+00:00',
    '2018-06-29 17:08:00.586525+05:00',
    'Tuesday , 6th September, 2017 at 4:30pm'
]

for date in date_array:  
    print('Parsing: ' + date)
    dt = maya.parse(date).datetime()
    print(dt)
    print(dt.date())
    print(dt.time())
    print(dt.tzinfo)
    print()


# Parsing: 2018-06-29 08:15:27.243860
# 2018-06-29 08:15:27.243860+00:00
# 2018-06-29
# 08:15:27.243860
# UTC

# Parsing: Jun 28 2018  7:40AM
# 2018-06-28 07:40:00+00:00
# 2018-06-28
# 07:40:00
# UTC

# Parsing: Jun 28 2018 at 7:40AM
# 2018-06-28 07:40:00+00:00
# 2018-06-28
# 07:40:00
# UTC

# Parsing: September 18, 2017, 22:19:55
# 2017-09-18 22:19:55+00:00
# 2017-09-18
# 22:19:55
# UTC

# Parsing: Sun, 05/12/1999, 12:30PM
# 1999-05-12 12:30:00+00:00
# 1999-05-12
# 12:30:00
# UTC

# Parsing: Mon, 21 March, 2015
# 2015-03-21 00:00:00+00:00
# 2015-03-21
# 00:00:00
# UTC

# Parsing: 2018-03-12T10:12:45Z
# 2018-03-12 10:12:45+00:00
# 2018-03-12
# 10:12:45
# UTC

# Parsing: 2018-06-29 17:08:00.586525+00:00
# 2018-06-29 17:08:00.586525+00:00
# 2018-06-29
# 17:08:00.586525
# UTC

# Parsing: 2018-06-29 17:08:00.586525+05:00
# 2018-06-29 12:08:00.586525+00:00
# 2018-06-29
# 12:08:00.586525
# UTC

# Parsing: Tuesday , 6th September, 2017 at 4:30pm
# 2017-09-06 16:30:00+00:00
# 2017-09-06
# 16:30:00
# UTC


dateutil과 다르게 maya는 로컬시간을 바라보지 않고 기본적으로 UTC로 지정합니다.

dateutil과 maya의 차이점

두 모듈은 비슷해 보이지만 timezone에 대해 아주 큰 차이가 있습니다.

import maya
import dateutil.parser

date_array = [  
    '2018-06-29 17:08:00.586525+05:00',
    '2018-06-29 17:08:00.586525',
]


for date in date_array:  
    print('Parsing: ' + date)
    maya_dt = maya.parse(date).datetime()
    dateutil_dt = dateutil.parser.parse(date)

    print('maya: ')
    print(maya_dt)
    print(maya_dt.tzinfo)

    print('dateutil: ')
    print(dateutil_dt)
    print(dateutil_dt.tzinfo)

    print()


# Parsing: 2018-06-29 17:08:00.586525+05:00
# maya: 
# 2018-06-29 12:08:00.586525+00:00
# UTC


# dateutil: 
# 2018-06-29 17:08:00.586525+05:00
# tzoffset(None, 18000)

# Parsing: 2018-06-29 17:08:00.586525
# maya: 
# 2018-06-29 17:08:00.586525+00:00
# UTC


# dateutil: 
# 2018-06-29 17:08:00.586525
# None


maya의 경우는 문자열에 존재하는 +05:00을 계산하여 timezone을 UTC로 세팅합니다. 위의 경우 17시에 +05:00가 존재하여 5시간을 뺀 후, timezone을 UTC라고 보여주고 있습니다. 반면 dateutil은 시간계산을 하지 않고 +05:00을 저장하게 됩니다. 

또한 timezone을 변경하거나 세팅할 때도 큰 차이가 존재합니다.

import maya
import dateutil.parser
import pytz
import dateutil

date = '2018-06-29 17:08:00.586525'

print('maya: ')
maya_dt = maya.parse(date)
print('parsing: ', maya_dt.datetime())
maya_dt = maya_dt.datetime(to_timezone='America/New_York', naive=False)


print('(timezone) America/New_York: ', maya_dt)
print(maya_dt.tzinfo)

print('------------------')

print('dateutil: ')
dateutil_dt = dateutil.parser.parse(date)
print('parsing: ', dateutil_dt)

timezone = pytz.timezone('America/New_York')
dateutil_dt = timezone.localize(dateutil_dt)

print('(timezone) America/New_York: ', dateutil_dt)
print(timezone)




# maya: 
# parsing:  2018-06-29 17:08:00.586525+00:00
# (timezone) America/New_York:  2018-06-29 13:08:00.586525-04:00
# America/New_York
# ------------------
# dateutil: 
# parsing:  2018-06-29 17:08:00.586525
# (timezone) America/New_York:  2018-06-29 17:08:00.586525-04:00
# America/New_York


maya의 경우, 파싱한 시간대에서 지정한 timezone의 시간만큼 계산하여 보여주지만 dateutil의 경우 datetime과 같이 시간은 계산하지 않고 timezone만 붙여서 보여주고 있습니다.

결론

문자열에서 datetime 객체로 변환하는 것은 third-party인 maya나 dateutil을 사용하는 것이 좋습니다. 또한 timezone을 변경하거나 각 다르게 사용할 경우, UTC로 변환하여 실행하는 서버 및 로컬의 시간대를 사용하지 않도록 하는 것이 중요합니다.

파이썬에서 어떤 값을 더할 때 +를 사용하여 새로운 변수에 결과값을 담을 수도 있고 기존에 사용하던 변수에 +=로 값을 대체할 때가 있습니다. 더 나아가 각 리스트를 합칠 때도 사용됩니다. 아래는 리스트를 합치는 예시입니다.

a = [1,2]
b = [3,4]
c = a+b
print(c)
# [1, 2, 3, 4]




a += b
print(a)


# [1, 2, 3, 4]


위 결과만 봤을 때 +와 +=의 차이가 없는 것처럼 확인됩니다. 그러나 아래 예시를 보면 똑같다 라는 가정이 틀렸다는 것을 볼 수 있습니다.

a = [1,2]
b = (3,4)
c = a+b

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "tuple") to list


a += b
print(a)

# [1, 2, 3, 4]


리스트와 튜플로 선언 후 + 연산자로 결과를 새로운 변수 c에 담으려 하면 에러를 반환하는 반면, += 연산자를 사용하여 a 변수에 결과를 반영하면 의도한대로 합쳐진 리스트가 나오는 것을 볼 수 있습니다.


여기서 + 는 __add__ 함수를 호출하고 +=는 __iadd__ 함수를 호출하게 됩니다. 

여기서 + 연산자는 대칭입니다. 대칭이라는 의미는 a+b와 b+a는 항상 같은 결과를 가져와야 합니다. 위와 같이 타입이 다른 경우, 연산자의 좌변과 우변의 선언 순서에 따라 타입이 변경되기 때문에 오류를 발생시킵니다. 

반면 += 연산자는 비대칭입니다. 선언문 왼쪽의 변수타입에 의해 결정이 됩니다. 따라서 a+=b는 a 타입을 유지하게 됩니다.


만약 b+=a 일 경우면 아래와 같은 결과를 반환합니다.

a = [1,2]
b = (3,4)
b += a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple


이처럼 +와 +=는 비슷해 보이지만 동작하는 방식이 다르므로 사용할 때 유의하여 사용해야 합니다.

합칠 대상의 결과가 2개 이상인 경우

union()

union()를 사용해서 1개로 합칠 수 있습니다. union()의 2번째 인자는 중복을 허용할지에 대한 여부인데 기본값은 False로 중복을 허용하지 않습니다. (중복데이터일 경우 중복제거)

result = a.union(b, all=True)

결과가 쿼리셋 리스트 타입이기 때문에 ORM 형식을 그대로 사용할 수 있습니다.

| 연산자

| 연산자를 사용하여 손쉽게 쿼리셋 타입 리스트를 합칠 수 있습니다.

result = a | b

결과가 쿼리셋 리스트 타입이기 때문에 ORM 형식을 그대로 사용할 수 있습니다.

+ 연산자

+ 연산자를 사용하기 위해선 각 쿼리셋 리스트 타입을 일반 리스트 타입으로 변경한 후 합칩니다.

a = list(a)
b = list(b)
result = a + b

합칠 대상이 단일일 경우

합칠 대상이 2개 이상이 아닌 1개만 존재하는 경우, 위의 방법을 전부 사용하지 못합니다.

chain()

chain() 함수를 사용하면 대상이 단일 건이거나 복수개여도 언제든지 합칠 수 있습니다.

from itertools import chain
result = list(chain(a, b))

list 변환 후 병합

쿼리셋 리스트 타입을 리스트로 변환 후, 단일건을 해당 리스트에 추가하는 형식입니다.

a = list(a)
a.append(b)

limit이 걸려있는 쿼리셋 리스트일 경우

limit이 걸려있는 경우에는 | 연산자 방식을 제외하고 전부 사용할 수 있습니다.

결론

병합에는 여러 방법이 있지만 가장 좋은 방법은 ORM에서 제공하는 union()과 chain() 입니다. 쿼리 결과를 병합하는 것을 지양해야 하지만 어쩔 수 없이 사용해야 한다면 union()과 chain()을 상황에 맞게 사용하면 됩니다.

+ Random Posts