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들만 모아놓은 인덱스를 기존 인덱스 위에서 만들 수 있으면 참 좋을텐데, 딱히 좋은 아이디어가 생기진 않는다.
결론
다른 데이터베이스의 색인보다 뭔가 좀 더 강력한 걸 만들 수 있을 것처럼 생기긴 했는데, 막상 아주 다른 무언가를 만들기엔 제약이 많아 쉽지 않다.