다방 클론 프로젝트를 하면서 아래와 같이 특정 위치의 주변 시설을 불러오는 기능을 작성해야했습니다.
다방의 API를 통해 서울지역의 편의시설/안전시설/학군정보에 대한 이름, 위치(경도,위도)는 DB에 넣어두었고, 이제 특정 방/단지 위치의 주변에 있는 시설들을 필터링해서 가져오면 됐습니다.
그러기 위해서는 먼저 특정 위치를 기점으로 기준으로 삼을 반경까지의 거리를 잴 수 있어야하고, DB에서 이 범위 안의 시설들을 필터링해서 반환해줘야합니다.
처음에는 이 거리를 계산하기 위해서 위도 경도간 계산법을 알아내 계산을 해야하나했는데, 다행히 파이썬 패키지 중에 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]
안녕하세요! 포스팅 잘봣습니다. 궁금한게 있어서 댓글 답니다~
에서 filter()에 의해서 결과값이 나오는데 all()를 추가하시는 이유가 있나요?
감사합니다~~