Django - 특정 위치 반경 정보 필터링하기(haversine으로 위도, 경도 거리 계산)

jomminii_before·2020년 3월 19일
4

다방 클론 프로젝트를 하면서 아래와 같이 특정 위치의 주변 시설을 불러오는 기능을 작성해야했습니다.

다방 캡쳐

다방의 API를 통해 서울지역의 편의시설/안전시설/학군정보에 대한 이름, 위치(경도,위도)는 DB에 넣어두었고, 이제 특정 방/단지 위치의 주변에 있는 시설들을 필터링해서 가져오면 됐습니다.

그러기 위해서는 먼저 특정 위치를 기점으로 기준으로 삼을 반경까지의 거리를 잴 수 있어야하고, DB에서 이 범위 안의 시설들을 필터링해서 반환해줘야합니다.

처음에는 이 거리를 계산하기 위해서 위도 경도간 계산법을 알아내 계산을 해야하나했는데, 다행히 파이썬 패키지 중에 haversine이라는게 있어서 이 과정을 대신해주었습니다.

PYPI Haversine 확인하기

Haversine은 간단히 >>> pip install haversine으로 설치할 수 있고 아래와 같이 사용할 수 있습니다. lyon이라는 곳과 paris 두 위치의 위도, 경도를 haversine 안에 넣어주면 두 위치 사이의 거리를 각 단위로 계산해줍니다. 디폴트는 kilometers로 되어있습니다.

### 공식문서 예시
from haversine import haversine, Unit

lyon = (45.7597, 4.8422) # (lat, lon)
paris = (48.8567, 2.3508)

haversine(lyon, paris)
>> 392.2172595594006  # in kilometers

haversine(lyon, paris, unit=Unit.MILES)
>> 243.71201856934454  # in miles

# you can also use the string abbreviation for units:
haversine(lyon, paris, unit='mi')
>> 243.71201856934454  # in miles

haversine(lyon, paris, unit=Unit.NAUTICAL_MILES)
>> 211.78037755311516  # in nautical miles

haversine을 사용해 작성한 근처 정보를 가져오는 뷰는 아래와 같습니다.

먼저 쿼리로 위치 정보를 받아 position이라는 변수에 저장합니다.

class NearInfoView(View):
    def get(self, request):
        try:
            longitude = float(request.GET.get('longitude', None))
            latitude  = float(request.GET.get('latitude', None))
            position  = (latitude,longitude)

그리고 나서 필터에 넣을 1차 조건을 작성합니다. 아무래도 DB에 저장된 전체 시설들을 필터링하면서 각 구분 값에 대한 결과물을 반환하는 것 보다 미리 일정 범위를 잘라놓은 후 그 안에서 필터링하는게 쿼리 속도가 더 빠릅니다.

저는 반경 2km를 기준으로 정보를 불러올 예정이라, 사방 1km씩을 미리 잘랐습니다. 위도 기준으로는 + 0.01이 약 +1km에 해당하고, 경도 기준으로는 +0.015가 약 +1km 정도에 해당합니다.

            condition = (
                Q(latitude__range  = (latitude - 0.01, latitude + 0.01)) |
                Q(longitude__range = (longitude - 0.015, longitude + 0.015))
            )


이렇게 범위를 줄이는 조건을 불러오고 싶은 객체에 필터로 적용하고, 필터된 객체와 특정 위치와의 거리가 2km이내인 객체를 모아서 반환합니다.

            convenience_infos = (
                ConvenienceInfo
                .objects
                .filter(condition)
            )
            near_convenience_infos = [info for info in convenience_infos
                                      if haversine(position, (info.latitude, info.longitude)) <= 2]


하나의 뷰 안에서 거리 기반 로직도 작성해보고, 사전 필터링으로 쿼리 속도를 줄일 수 있는 경험도 해봐서 좋았던 코드였습니다.

필터링까지만 적용된 전체 코드는 아래와 같습니다.

class NearInfoView(View):
    def get(self, request):
        try:
            longitude = float(request.GET.get('longitude', None))
            latitude  = float(request.GET.get('latitude', None))
            position  = (latitude,longitude)
            condition = (
                Q(latitude__range  = (latitude - 0.01, latitude + 0.01)) |
                Q(longitude__range = (longitude - 0.015, longitude + 0.015))
            )

            convenience_infos = (
                ConvenienceInfo
                .objects
                .filter(condition)
            )
            near_convenience_infos = [info for info in convenience_infos
                                      if haversine(position, (info.latitude, info.longitude)) <= 2]

            safety_infos = (
                SafetyInfo
                .objects
                .filter(condition)
            )
            near_safety_infos = [info for info in safety_infos
                                 if haversine(position, (info.latitude, info.longitude)) <= 2]

            education_infos = (
                EducationInfo
                .objects
                .filter(condition)
            )
            near_education_infos = [info for info in education_infos
                                    if haversine(position, (info.latitude, info.longitude)) <= 2]
profile
https://velog.io/@jomminii 로 이동했습니다.

5개의 댓글

comment-user-thumbnail
2020년 3월 19일

안녕하세요! 포스팅 잘봣습니다. 궁금한게 있어서 댓글 답니다~

  1. EducationInfo.objects.filter(condition).all()
    에서 filter()에 의해서 결과값이 나오는데 all()를 추가하시는 이유가 있나요?
  1. 디비가 공간 쿼리를 지원해도, 장고에서 공간 쿼리를 사용하려면 raw query 말고는 포스팅과 같이 다른 패키지를 써야 하는건가요?! (장고 모델 쿼리에서 지원하는걸 찾지 못하겠네요.)

감사합니다~~

1개의 답글
comment-user-thumbnail
2022년 5월 19일

혹시 위도 경도 값은 준비를 해야 사용할 수 있나요?

답글 달기
comment-user-thumbnail
2023년 7월 25일

안녕하세요:) 사용하신 코드를 적용해야해서, 참고했는데 'Convenience_Infos.objects' 가 의미하는게 request 를 통해 받아온 url 태그인건지 궁금합니다!

답글 달기