Tag Archives: 루씬

검색엔진 Solr로 교체

여태까지 회사에서 운영중인 서비스에서 CLucene 기반으로 제작한 자체 검색서버를 사용했었는데, 이번에 Solr로 교체합니다. 교체 이유는 가끔씩 서버가 죽는 경우가 생기는데 원인파악이 안되더군요. 주로 인덱싱을 열심히 시키면 죽고, 인덱싱하는 동안 검색서비스가 많이 느려지더군요. 그래서 실시간 인덱싱이 필요한 경우를 제외하고는 하루에 한번씩 한가한 시간에 전체 인덱싱을 오프라인으로 해서 인덱스 디렉토리를 교체하는 형태로 서비스를 하고 있었습니다. 이렇게 운영하면서 서비스에 크게 문제가 있지는 않았지만, C++로 작성하다보니 새로운 요구사항이 들어올때 제가 직접 일을 처리하게 되더군요.

예전엔 Solr라는 거 자체가 있었는지 몰랐었고, 자바로 되어 있는 루씬은 좀 거부감이 있었는데.. Solr의 기능들을 보니 자체구현한 검색엔진을 유지보수/확장하는 거보다 여러가지 장점들이 눈에 보이더군요. 먼저… 유지보수 측면에서 XML만 편집하면, 스키마를 업데이트할수 있는 장점이 있고, 리플리케이션 또한 꼭 필요한 기능이라는 생각이 들더군요. 그리고 HTTP 기반으로 돌기 때문에 여러 검색서버를 띄우고 로드밸랜싱도 가능하겠더군요… 그리고 성능최적화를 위한 기능들과 문서들… 아직 제대로 보지는 못했지만 다른 이들의 경험에서 배울수 있는것이 많을거라 생각했습니다.

디비와 Solr의 동기화는 Python을 이용해서 직접 작성했습니다. Python으로 디비에서 필드들을 읽어들여 XML화하여 Solr에 저장했습니다. 예전엔 디비의 업데이트가 일어나는 부분에서 인덱싱을 다시 하도록 로직을 짰었는데… 이게 관리가 잘 안되더군요. phpmyadmin에서 디비 편집하는 경우도 있고… 예전엔 하루에 한번씩 전체 인덱싱을 다시하여 이런 문제를 해결했었는데, 좀더 근본적인 대책을 찾아야겠더군요. MySQL 5.0의 트리거를 사용하여서 변화가 있는 데이타를 추적하여 자동으로 디비와 Solr와 동기화 되게 했습니다. 현재 사용중인 MySQL의 버전이 대부분 4.X라 검색서버에 MySQL 5를 설치하고, 필요한 테이블만 리플리케이션 되게 세팅했습니다. (참조하는 마스터 디비가 두개라 하나의 머신에 여러개의 MySQL 인스턴스를 돌리는 방법을 이용했습니다.) 그리고 트리거를 설치하여 변화가 있는 부분을 _changed_에 쓰도록 했습니다.

create table _changed_ (no int NOT NULL auto_increment,
id varchar(100) character set utf8,
type varchar(100) character set utf8,
action varchar(100) character set utf8,
date datetime NOT NULL,
KEY no (no));

create trigger tr_news_insert after insert on news
for each row INSERT INTO _changed_ (id, type, action, date) values (NEW.no, “news”, “index”, NOW());

create trigger tr_news_update before UPDATE on news
for each row INSERT INTO _changed_ (id, type, action, date) values (NEW.no, “news”, “index”, NOW());

create trigger tr_news_delete before DELETE on news
for each row INSERT INTO _changed_ (id, type, action, date) values (OLD.no, “news”, “delete”, NOW());

crontab에서 1분에 한번씩 _changed_ 테이블을 보고 Solr에 변경된 사항을 적용하도록 했습니다.

리플리케이션은 rsyncd를 이용하도록 되어있어서 루씬처럼 업데이트 항목만 다른 파일로 관리되는 경우 효율적으로 동기화가 가능합니다. 다만 optimize할때는 파일들 모아서 전체를 다시 쓰기 때문에 마스터에서 자주 optimize하는거는 추천하지 않습니다.

마스터에서 commit할때 snapshooter를 호출하도록 하고, slave에서는 5분에 한번씩 snappuller와 snapinstaller를 호출하도록 했습니다. slave는 따로 데이타를 복사할 필요가 없어서 slave 폴더만 압축해두면 아주 빠르고 쉽게, 동기화와 서비스를 할수 있더군요. 디비 리플리케이션이 이렇게 될수 있다면.. 이라는 생각이 드네요.

php에서는 phps 포맷으로 검색결과를 받아가도록 했습니다.

[CODE type=php]
<?php
   function solr_search_get($args) {
     $sr = file_get_contents(“http://solrserver:solrport/solr/select?”.$args.”&wt=phps”);
     return unserialize($sr);
   }
   print_r(solr_search_get(“q=hello”));
?>
[/HTML][/CODE]

자체 제작 CLucene 검색서버와 Solr의 퍼포먼스는 … 차이가 좀 납니다. 다른 서버에서 돌려서 정확한 비교는 어렵지만, 검색속도의 차이는 크지 않습니다. Solr 서버의 경우 최근에 들어온 서버인데 성능은 비슷합니다. 인덱싱은 큰 차이가 납니다. 자체 검색 서버의 경우 20분이면 전체 인덱싱을 하는데, Solr에서 50분 정도 걸립니다. 아마도 XML로 변환되고, HTTP로 데이타가 들어가야해서 속도차이가 많이 나는듯합니다. XML 변환을 python으로 하는 것도 성능에 영향을 주었을듯 하네요.

CLucene CJK 분석기 (win32)

전에 CLucene CJK 분석기 리눅스용 소스를 공개하고 윈도우용도 공개하기로 했었는데, 계속 작업 안하고 있다가 마침 요청이 들어와서 정리했습니다.

압축파일에는 clucene-core-0.9.16a가 포함되어 있지 않으며, 압축을 풀고 아래와 같은 폴더 구조로 만들어주시면 됩니다. clucene 소스는 직접 받으셔야 합니다.


CLuceneTest 폴더에 프로젝트(dsw) 파일이 포함되어 있으며, Release에 실행파일을 포함했습니다.

실행을 하면 아래와 같이 간단한 창이 뜨며, 위쪽 텍스트를 라인별로 인덱싱하며, 가운데 검색창에서 쿼리를 입력하고 검색하면 아래 검색 결과가 나타납니다. 검색 버튼을 누를때 위쪽 텍스트에 변경 사항이 있으면 인덱싱을 한후 검색이 됩니다.

윈도우즈에서 CLucene 예제를 만들면서 몇가지 문제점이 있었습니다.

Visual Studio에서 위저드 통해서 MFC 프로젝트 만들면 Preprocessor에서  _MBCS가 정의되어 있는데, 이게 있으면 CLucene이 잘 컴파일이 안됩니다. 두가지 해결방법을 찾긴 했는데 둘다 근본적인 해결책은 아닌거 같네요.

첫번째 방법은 프로젝트 전체에서 _MBCS 정의를 빼버리는 방법이고, 두번째 방법은 CLucene의 “StdHeader.h” 에서 #undef _MBCS 하는 방법입니다. 두번째 방법은 clucene 소스를 한줄 고쳐야하기 때문에 제가 올리는 샘플 프로젝트에서는 첫번째 방법을 사용했습니다.

컴파일 할때 주의 사항은 CLucene에 해당하는 C++ 소스는 “CLucene/StdHeader.h” 파일을 precompiled header로 사용하도록 해야합니다. UI 부분과 Lucene 부분을 분리하여서 UI 쪽에서 Lucene 함수에 직접 접근하지 않도록 했습니다.

컴파일중에 다음 경고가 뜨긴 하는데 실험해본결과 퍼포먼스에 큰 문제는 없었습니다. (제가 사용한 응용 프로그램 기준)

==================Hashing not available or is disabled! CLucene may run slower than optimal ==================

그리고 디버그로 컴파일할때 internal compiler 오류가 납니다.

Fatal error C1076: compiler limit : internal heap limit reached; use /Zm to specify a higher limit

아래처럼 프로젝트 C++ 옵션에서 맨 아래 수동으로 /Zm400 옵션을 추가해주면 문제가 해결됩니다.

소스입니다.

1302547880.zip

CLucene CJK 분석기

CLucene을 이용하여 검색엔진 구현하는데, 한글처리에 대한 정보가 거의 없더군요. 아주 기본적인 한글처리만 구현해보았습니다. CLucene에서 한글 처리에 대해서 참고하시면 도움이 되리라 생각하여 소스를 공개합니다.

리눅스와 윈도우즈에서 동작하지만 먼저 리눅스 소스만 공개합니다. 윈도우즈에서 아직 _MBCS 정의를 빼지않고 컴파일에 성공하지 못했습니다. 좀더 연구해봐야할듯하네요. 소스는 코드변환 외에는 차이가 없습니다.

clucene-core-0.9.16a 버전을 사용했으며 Makefile에서 CLUCENEPATH를 설정하고 make하시면 됩니다. 소스에 포함된 한글은 UTF-8로 인코딩되어있으며, CentOS 4.4 AMD64 리눅스(LANG=ko_KR.UTF-8)에서 테스트했습니다.

clucene의 StandardTokenizer에 보면 CJK관련 처리가 있지만, next()에서 _CJK로 인식하기 전에 다른곳(_istalpha)으로 빠져서 CJK 토큰으로 분류가 안되더군요. 그래서 복사해서 CJKTokenizer.cpp를 만들고 비교 순서만 바꿔줬습니다. 왜 한글코드가 _istalpha으로 인식되어 빠져나가는지는 잘 모르겠네요.

KoreanStemFilter.cpp에서는 CJK 토큰을 2글자 단위로 나누는 역할을 합니다. 루씬인액션에 설명되어있는데 clucene에는 구현이 안되어 있는거 같더군요. “검색엔진” 토큰을 “검색” “색엔” “엔진” 토큰으로 바꾸죠. 한글의 조사를 뺀다던가 하는 기능을 추가하기 위해서 KoreamStemFilter로 만들었는데 지금 기능은 CJK 필터만 구현되어있네요.

ConvertUtil.cpp는 iconv를 이용하여 UTF-8을 UTF-32LE로 바꾸는 소스 입니다. 윈도우즈의 경우는 MultiByteToWideChar()와 WideCharToMultiByte() 함수를 이용했습니다.

CLuceneTest.cpp는 간단한 데이타 3개를 넣고 터미널 상에서 검색할 수 있는 테스트 프로그램입니다. clucene의 데모 소스를 약간 바꿔서 구현했습니다.

다음은 실행한 화면입니다:

$ ./CLuceneTest
adding doc: doc1 – hahaha 한글단어 hohoho 비 bye 검색엔진
adding doc: doc2 – hello zaza 한글 김현정 김건 건모 검색
adding doc: doc3 – goodbye 김건모 서영은 검색 엔진 SG워너비
Indexing took: 5 ms.

Enter query string: 검색엔진
Searching for: “검색 색엔 엔진”

0. doc1 – hahaha 한글단어 hohoho 비 bye 검색엔진 (0.974307)

Search took: 1 ms.
Screen dump took: 0 ms.

Enter query string: +검색 +엔진
Searching for: +검색 +엔진

0. doc1 – hahaha 한글단어 hohoho 비 bye 검색엔진 (0.383675)
1. doc3 – goodbye 김건모 서영은 검색 엔진 SG워너비 (0.383675)

Search took: 0 ms.
Screen dump took: 0 ms.

소스입니다.
1204400252.tgz

검색 기술 좌충우돌

검색 기술은 제대로 배워본적이 없었는데, 서비스 운영하다보니 여러번 발목을 잡더군요. 서버에서 검색 기술이 필요한 부분도 있었고, 클라이언트에서도 필요했습니다.

클라이언트 단에서는 처음에는 인덱스 없이 직접 스트링 비교를 해서 검색하다가, 검색할 데이타가 늘면서 느려져서, 검색 인덱스를 메모리에 간단히 만들고 (stl map 이용), 사용하니 큰 문제없이 잘 동작하더군요. 근데 경우에 따라서 검색 인덱스가 아주 커지는 클라이언트 들이 생겨서 메모리에 전체 인덱스를 올리는것이 불가능한 경우가 생겼습니다. 그래서, 어쩔수 없이 파일 기반으로 map처럼 사용할수 있는 dbm 계열의 라이브러리를 사용하여 인덱스를 저장하여 사용하고 있습니다. 현재까지는 큰 문제없이 사용하고 있습니다. 하지만 검색 인덱스가 커지고 쿼리가 늘면 좀 버벅대더군요. 아무래도 인덱스 만드는거나 검색자체가 효율적으로 구현한것이 아니고, 저장하는 방법 또한 효율적으로 검색할수 있는 구조가 아니었던것 같네요.

서버 단에서는 디비에 있는 필드 검색이라 MySQL의 FullText 검색을 이용할 방법을 고민했었습니다. 요즘도 해결 안됐을거 같은데 FullText 검색은 한글을 지원하지 않죠. 그래서 고민하다가 생각한 방법이 한글을 영문으로 인코딩하여 필드하나 더 만들어서 박아넣는거였습니다. 일단 영문은 다 소문자로 변경해서 넣고, 한글은 대문자로 자~알 인코딩 해서 넣고, 디비 Insert,Update할때 인덱스 필드에 변환해서 넣고, 검색할때 한글 변환해서 fulltext search하고.. 이렇게 하니 잘 동작은 하더군요. 몇달간 서비스하면서 데이타가 몇기가 쌓이니, 점점 느려져만 가더군요. 이건 서비스 기획의 문제였지만, 운영하던 서비스는 거의 삭제의 개념이 없고 계속 누적되는 형태로 운영이 되었습니다. 그러니 문제가 될수 밖에 없었죠. 나중에는 검색을 새로 구현할 엄두가 나지 않아서 검색 자체를 빼고 P2P 검색으로 대체했습니다…

서버 단에서 다시 검색이 필요하여, MySQL fulltext보다는 좀더 좋은 방법을 찾으려고 했는데… 인덱싱하는 페이지들이 모두 html로 만들어져 있기 때문에, 웹기반 인덱싱 쪽으로 알아보았습니다. 외국에서 만든 많은 검색엔진들이 단어 단위로 인덱스를 만들기 때문에 한글 검색이 잘 안되는 경우가 많더군요. “검색엔진”이라는 단어가 인덱싱 되면 “검색”으로는 해당 문서가 검색이 안되는 엔진들이 많더군요. mnogosearch라는 검색엔진을 발견했는데, 한글 검색이 완벽하게 되더군요. 구현할 시간은 없고 일단 동작을 하다보니 그냥 사용하기로 하고 서비스 했습니다 –; 뭐 처음에는 사용자들이 검색 들어갔는지도 잘 몰랐기 때문에, 서비스 운영에 전혀 문제 없었습니다. 데이타가 쌓이고, 검색 많아지고 하면서 문제가 조금씩 생기더군요. 일단 서버 여러대에 분산 처리했습니다. 근데 문제는 몇몇 사용자가 검색을 집중적으로 해도 서비스가 상당히 불안해졌습니다. 서비스가 다운되거나 그런건 아니지만, 디비에 부하가 걸려서 응답속도가 느려지더군요. mnogosearch에 대한 최적화 방법 같은걸 찾아보고, 들어오는 쿼리들을 저장하고 벤치마킹을 해봤습니다. 처음 벤치마크 결과는 절망적이더군요. 일단 쿼리에 따라 검색시간에 큰 편차가 있었습니다. 내부적인 동작을 모르니, 최적화는 한계가 있을거 같다는 생각을 하고 mnogosearch를 대체할수 밖에 없다고 결론을 냈습니다.

로레벨(ㅎㅎ) 개발자이기 때문에 먼저 위키피디아에서 검색 알고리즘들을 찾아보고, 필요한 소스코드(주로C)를 수집하기 시작했습니다. 알고리즘 소스들을 연결하여 인덱싱하고 검색하는거 구현하는 거는 정말 쉬운일이 아닐거 같다는 생각이 금방 들더군요. 그래서 좀더 검색하다 찾은것이 루씬(Lucene)입니다.

루씬은 자바로 된 검색엔진입니다. 검색엔진이라는 용어 자체가 좀 애매한데, mnogosearch는 사용자를 위한 검색 엔진이라면, 루씬은 개발자용 검색 엔진입니다. mnogosearch는 인덱스할 url을 입력하고, 화면에 검색결과가 어떻게 표시될지만 설정해주면 바로 사용이 가능합니다. 루씬의 경우는 검색엔진 라이브러리입니다. 개발자가 직접 문서를 읽어들이는 코드, 인덱싱하고, 검색하는 코드를 직접 작성해야합니다. 또한 어느정도 기술에 대한 이해도가 있어야 좋은 성능을 낼수 있다고 하네요. 다행히 Lucene in Action이라는 책이 나와있어서 큰 도움이 되더군요. 번역판도 나온거 같더군요…

제가 자바를 선호하지 않고, 잘 동작한다면 클라이언트(win32)단에서도 사용하고 싶었기 때문에 C++로 포팅된 CLucene을 사용하기로 결정했습니다. CLucene이 자바 루씬에 비해서 버전이 낮긴하지만, 성능도 뛰어나고, 제가 C++이 더 익숙해서요…. win32 클라이언트는 종속성을 줄이기 위해서 static 링크되지 않는 외부라이브러리는 가능하면 안씁니다. ( 특히 .net 같은거는 절대 안씁니다. )

CLucene으로 현재 검색 서버를 개발 완료했으며, 클라이언트 단에도 실험적으로 돌려보고 있는데 성능은 아주 잘 나오고, 검색 시간 편차는 정말 적더군요. 서버단에서 벤치마크 결과 아주 만족스런 결과가 나왔습니다.

시간내서 CLucene으로 한글 검색 되도록 스탬필터로 개발한 소스를 공개하도록 하겠습니다. 한글스탬필터는 2글자씩 짜르는거 외에 별도 처리는 구현하지 않았습니다. 자바 루씬에는 이렇게 구현된 것이 들어있는지 모르겠는데, CLucene에서는 소스 찾아봐도 없더군요. 그리고 CLucene의 StandardAnalyzer로는 CJK 처리가 정상적으로 동작안하더군요. (win32, linux에서 모두)

소스 조만간 공개하겠습니다 ^^