문서에 좌표를 붙여 두고, 사용자 위치를 기준으로 거리 조건을 걸거나 거리를 점수에 반영하는 검색이다. “내 주변 카페”, “반경 3 km 안의 병원” 같은 검색이 여기에 속한다.

가게가 수백만 곳이면, 쿼리마다 모든 가게와의 거리를 계산하는 것만으로 응답 시간이 다 간다. 그래서 위치 검색은 보통 두 단계로 나눈다.

flowchart TD
    Q["사용자 위치 + 검색어"] --> F["① 후보 줄이기: 반경 N km 안의 문서만 남긴다<br/>(geo_point 공간 색인, geo_distance 필터)"]
    F --> S["② 점수에 섞기: 텍스트 관련도 × 거리 점수<br/>(function_score 감쇠 함수)"]
    S --> R["필요하면 랭킹 모델이 거리를 feature 로 받아 다시 정렬"]
    style F fill:#dbe8f8
    style S fill:#fbe1d6

첫 단계는 대부분의 문서를 거리 계산 없이 통째로 버리는 일이다. 공간 색인이 이 일을 맡는다. 두 번째 단계는 남은 후보에서 가까울수록 점수를 더 주는 일이다. 여기서는 거리를 직접 계산하지만, 대상이 수천 개로 줄어든 뒤라 부담이 작다.

후보 줄이기: geo_point 와 BKD Tree

Elasticsearch 에서 좌표는 geo_point 필드로 색인한다. Lucene 은 이 좌표를 BKD tree 라는 공간 색인에 넣는다. 숫자 · 날짜 범위 검색에 쓰는 것과 같은 계열의 구조다 (Lucene Segment 의 Points 항목).

BKD tree 는 k-d tree 를 디스크 블록 단위로 저장하도록 바꾼 것이다. 점들을 위도와 경도 중 하나를 골라 중앙값으로 반씩 나누기를 반복해, 리프 칸 하나에 점이 일정 개수만 남게 만든다.

반경 검색이 들어오면 트리를 위에서부터 내려가며 칸마다 원과의 관계를 본다.

칸과 원의 관계처리
칸이 원 밖에 있다그 아래를 더 내려가지 않는다. 안의 점은 한 번도 읽지 않는다
칸이 원 안에 통째로 들어간다안의 점을 거리 계산 없이 모두 결과에 넣는다
칸이 원의 경계에 걸친다이 칸의 점만 하나씩 거리를 계산한다

그림에서는 점 197 개 중 58 개만 거리를 계산했다. 실제 데이터는 점이 수백만 개이고 반경은 그중 작은 영역이라, 원 밖이라서 통째로 건너뛰는 칸의 비율이 그림보다 훨씬 크다. 그림의 칸에는 점이 몇 개뿐이지만, 실제 BKD tree 의 리프 칸에는 수백 개씩 들어간다.

이 단계는 geo_distance 쿼리를 filter 안에 넣어 쓴다. filter 는 점수를 매기지 않고 통과 여부만 정하므로 점수 계산 비용이 없다.

{
  "query": {
    "bool": {
      "must": { "match": { "name": "카페" } },
      "filter": {
        "geo_distance": {
          "distance": "3km",
          "location": { "lat": 37.5665, "lon": 126.9780 }
        }
      }
    }
  }
}

점수에 섞기: function_score 감쇠 함수

반경 안에 들어온 문서끼리도 가까운 쪽이 더 낫다. function_score 의 감쇠(decay) 함수는 거리를 0 과 1 사이의 점수로 바꾸고, 이 점수를 텍스트 관련도 점수에 곱한다.

감쇠 함수는 네 값으로 모양이 정해진다.

파라미터뜻
origin거리를 재는 기준점. 보통 사용자 위치
offset이 거리 안은 감쇠하지 않고 1 을 준다
scaleoffset 에서 이만큼 더 멀어지면 점수가 decay 가 된다
decayoffset + scale 지점의 점수. 기본값 0.5

함수 모양은 gauss, exp, linear 셋 중에 고른다. 세 함수 모두 offset + scale 지점에서 decay 를 지나지만, 가까운 곳과 먼 곳에서 기울기가 다르다.

  • gauss: 가까운 곳에서는 천천히 줄다가 중간에서 빠르게 떨어진다. 걸어갈 수 있는 거리 안은 거의 같게 보고 싶을 때 맞다.
  • exp: 처음부터 빠르게 줄지만, gauss 보다 천천히 0 에 가까워진다. 먼 문서에도 거리 점수가 꽤 남는다.
  • linear: 일정한 기울기로 줄다가 어느 거리에서 0 이 된다. 그 너머는 거리 점수가 0 이라 곱하면 문서 점수도 0 이 된다.

gauss 의 식은 다음과 같다. 는 offset + scale 지점에서 점수가 decay 가 되도록 정해진다.

는 문서와 origin 사이의 거리다.

{
  "query": {
    "function_score": {
      "query": { "match": { "name": "카페" } },
      "functions": [
        {
          "gauss": {
            "location": {
              "origin": { "lat": 37.5665, "lon": 126.9780 },
              "offset": "0.5km",
              "scale": "2km",
              "decay": 0.5
            }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

boost_mode 가 multiply(기본값)면 최종 점수는 텍스트 점수 × 거리 점수다. sum 으로 바꾸면 더하는데, 텍스트 점수의 범위가 쿼리마다 달라서 거리 점수의 영향이 들쭉날쭉해진다.

거리를 랭킹 모델에 넣기

감쇠 함수는 사람이 정한 곡선이다. 학습 없이 바로 쓸 수 있는 출발점이지만, 카테고리마다 거리에 대한 민감도가 다르다는 사실을 반영하지 못한다. 커피는 500 m 만 멀어도 안 가지만, 잘 맞는 병원은 5 km 를 간다.

Learning-to-Rank 모델을 쓰는 서비스라면 거리를 다른 feature 와 함께 넣고, 클릭 · 방문 로그로 학습시킨다. 트리 모델은 “카테고리가 병원이면 거리 영향을 줄인다” 같은 조합을 스스로 찾는다. 이때도 첫 단계의 반경 필터는 그대로 둔다. 랭킹 모델은 수천 개 후보를 다시 정렬할 뿐, 수백만 개 전체를 볼 수는 없기 때문이다.

References