CouchDB에서 시작하는 인덱스 전설


서론

요새 부동산을 보러 돌아다니다가 이 사이트랑 저 사이트가 보여주는게 다르니 내가 보고 싶은 정보들을 정리하기 위한 목적으로 다음과 같은 툴을 만들고 있다.

  • 리얼터랑 같이 쓰는 웹사이트에서 즐겨찾기 등을 관리
  • 즐겨찾기에 추가된 매물에 대해 zillow / redfin에서 열어볼 수 있는 링크 생성

문제 - 내가 원하는 인덱스

그 ‘리얼터랑 같이 쓰는 웹사이트’에서 즐겨찾기 정보를 긁어올 수 있다 치자. zillow와 redfin에서 임의의 주소로 검색할 수 있는 기능을 만들었다 치자. 그러고 나면 내가 원하는 기능을 대충 SQL 같은 언어로 표현하자면,

SELECT 매물
FROM 즐겨찾기
WHERE zillow_link IS NULL OR redfin_link IS NULL

같은 녀석이 될 텐데, 이걸 CouchDB에서 비슷하게 흉내내보고 싶다… 는게 오늘의 목표라고 할 수 있다.

참고로, CouchDB에 저장하는 문서의 형식은 다음과 같다:

interface ResultDocument {
  type: "result";
  sessionId: string;
  address: string;
  absoluteUrl: string;
}

export interface LookupDocument {
  type: "lookup";
  source: "zillow" | "redfin" | "trulia";
  address: string;
  url: string | null;
  date: Date;
}

CouchDB? View?

CouchDB에서 특이한 점은, 색인을 map/reduce의 조합으로 직접 구현할 수 있다는 점이다. 어떤 문서 X에 대해 map(X) := List[Key -> Value]를 정의하고, reduce(List[X]) := Value 함수를 정의하면, CouchDB에서 해당 함수를 조합해서 초기 인덱스를 생성하고, 이후 변경사항에 대해서는 필요한 부분만 재계산해서 인덱스를 최신 상태로 유지하겠다…는 것이다.

Show me the example

function map(doc) {
  // result / lookup 문서에 대해 각각 색인 항목을 생성
  if (doc.type === "result") {
    emit([doc.address, doc._id], {"base": doc.absoluteUrl });
    return;
  }
  if (doc.type === "lookup") {
    emit([doc.address, doc._id], {[doc.source]: doc.url})
  }
}

function reduce(keys, values, rereduce) {
  // 같은 주소의 색인 문서가 여러개 생기는 경우 단순히 합쳐줌
  return values.reduce(function (a,b) { return Object.assign({}, a, b); });
}

address가 join key같은 느낌으로 ResultDocument와 그에 해당하는 LookupDocument를 묶어주고, 인덱스를 쭉 읽으면서 원하는 LookupDocument가 없는 key들을 찾아서 lookup을 수행할 생각으로 만들어봤다.

만들고 보니…

현재는 만들어진 인덱스를 처음부터 끝까지 O(N)으로 돌면서 필요한 LookupDocument가 있는지 없는지 확인해야 해서 아주 효율적이라고는 할 수 없을것 같다. 아마 Lookup 결과를 별도 문서로 저장하는게 아니라 ResultDocument를 업데이트하는 방식으로 구현했으면 map에서 바로 Lookup 필요 여부를 판단할 수 있을테지만, 문서들을 가급적 딱 한번 생성만 하고 변경이 없는 구조로 만들고 싶어서 그렇게 하진 않고 있다.

LookupDocument가 없는 key들만 모아놓은 인덱스를 기존 인덱스 위에서 만들 수 있으면 참 좋을텐데, 딱히 좋은 아이디어가 생기진 않는다.

결론

다른 데이터베이스의 색인보다 뭔가 좀 더 강력한 걸 만들 수 있을 것처럼 생기긴 했는데, 막상 아주 다른 무언가를 만들기엔 제약이 많아 쉽지 않다.