( 월간 마이크로 소프트웨어 8월 연재 )
Apache Lucene은 Doug Cutting에 의해 순수 JAVA로 개발된 full-text 검색 엔진이다. 아파치 자카르타의 서브 프로젝트로 개발되어 오다 현재는 아파치 최상위 프로젝트로 승격되었으며, 너치(nutch)라는 자식 프로젝트 까지 갖춘 소위 대박난 오픈 소스 프로젝트 이다. 동급(아파치 프로젝트 레벨)의 다른 프로젝트에 비해 국내 개발자들 에겐 인지도가 무척 저조한 편이라 Lucene이 적용된 레퍼런스 조차 제대로 찾아보기 힘들 지만 Apache Lucene 프로젝트는 나날이 발전 되어서 현재는 C++, C#, Python, Perl 과 같은 여러 다른 언어로도 포팅 되어 널리 이용되고 있다.
---------------------------------------------------------------------------------------------------------------- 너치(nutch)는 lucene의 개발자인 Doug Cutting이 역시 수석 개발을 맡아 진행하는 lucene을 기반으로 한 오픈 소스 프로젝트로 구글과 같은 대형 검색 서비스사의 독점을 막고, 누구나 쉽게 사용하고 공유할 수 있는 오픈 소스 검색엔진을 만든다 라는 취지 하에 개발되게 되었고, 2005년 1월 아파치 인큐베이터 프로젝트에 소속되었다가 최근 탑 레벨 프로젝트인 lucene 의 서브 프로젝트로 승격 하게 되었다. (http://lucene.apache.org/nutch/ )
----------------------------------------------------------------------------------------------------------------
1. Lucene의 탄생 Lucene 은 1997년 Doug Cutting의 개인 프로젝트로 시작된 그의 4번째(Xerox, Apple , Excite & Lucene) 검색 소프트웨어 이다 . 믿기지 않는 사실 이긴 이지만 그가 작성한 최초의 자바프로그램 이였다고 하니 수년간이나 자바공부에 전념을 해도 이렇다 할 진전이 없었던 우둔한 필자의 입장에선 부끄러움과 함께 절로 존경심이 생기지 않을 수 없다. 처음 Lucene을 개발하던 당시에는 이 제품을 상용화 하려던 의도를 가지고 있었다고 한다. 하지만 곧 생각을 바꿔 sourceforge에 공개 함으로서 삽시간에 전세계 개발자에게 퍼지게 되었고 1년여 정도가 지나 아파치 재단에 채택되면서 Lucene은 말 그대로 개발의 날개를 달게 되었다. (실제로 Lucene의 로고는 날개 형상과 매우 흡사하게 생겼다.) 그리고 현재는 아파치 탑 레벨 프로젝트로 승격되었고, 여러 개발 언어로 번역되어 전세계 개발자에게 널리 퍼지면서 나중에 소개할 Luke 와 Limo 같은 서드파티(third-party) 툴까지 마구 양산 되면서 개발자를 날로 즐겁게 해주고 있다.
Version |
Release date |
이력 |
0.01 |
2000년 3월 |
최초 오픈소스 release ( sourceforge) |
1.0 |
2000년 10월 |
|
1.01b |
2001년 7월 |
마지막 sourceforge release |
1.2 |
2002년 6월 |
Apache Jakarta release |
1.3 |
2003년 12월 |
Compound index format, QueryParser 개선, remote searching, token positioning, extensible scoring API |
1.4 |
2004년 7월 |
Sorting, span queries, term vectors |
1.4.1 |
2004년 8월 |
버그 픽스( sorting performance) |
1.4.2 |
2004년 10월 |
IndexSearcher optimization 과 기타 버그 픽스 |
1.4.3 |
2004년 겨울 |
기타 수정 |
<표 1> Lucene Release History
2. Lucene 의 활용 검색엔진 이라 하면 아주 고가의 상용 솔루션을 먼저 떠올리던 시절이 있곤 했다. 하지만 Doug Cutting 과 여러 오픈 소스 개발자들의 노력으로 어느새 모든 개발자들은 문서를 Indexing 하고 Searching 하는 능력을 별다른 노고 없이 ( 솔직히 API를 살펴보는 최소한의 노고는 필요 할 것이다.) 갖출 수 있게 되었다. 이제 이 파워풀 한 능력을 어디에다 써먹을 수 있을까? 기본적인 문서 검색에서 시작해 이메일, CD컨텐츠, xml, 데이터베이스, 웹사이트 등등 무궁무진하게 많은 영역을 다룰 수 있을 것이다. 하지만 여기에도 한계는 있었다. 필자의 경우 공공기관 관련 SI프로젝트 에서 Lucene 검색 엔진을 도입 하려 했을 때 단지 오픈 소스 라는 이유로 혹은 지원이나 문제 발생시 책임 소지 등을 거론하며 냉대 받고 결국은 훨씬 성능이 떨어지면서 사용하기도 불편한 고가의 검색 엔진 솔루션을 구입해서 프로젝트를 진행했던 경험이 있었다. 그 후 다시 기회가 찾아 왔을 땐 구글의 데스크탑 검색과 Lucene.net (Lucene의 닷넷 버전)을 이용한 Microsoft의 email 검색 소프트웨어인 Lookout(그림1) 을 레퍼런스로 들면서 열심히 고객을 설득했고, 결국에 우리팀은 Lucene을 이용해 프로젝트에서 빈번하게 DB접속이 일어나 성능을 저하 시키는 모든 요소를 Lucene 검색엔진 으로 대처 하였고 대용량 DB의 like검색으로 인한 과부하를 적절하게 해소 할 수 있었다.
--Doug Cutting이 제시한 lucene의 인덱싱과 검색을 적용 가능한 일반적인 사례 --- " 이메일 검색: 저장된 메시지를 검색할 수 있고 새로 도착한 메시지를 새인에 추가할 수 있는 이메일 애플리케이션. " 온라인 문서 검색: 온라인 문서 또는 저장된 출판물을 검색할 수 있는 CD 기반이나 웹 기반 또는 애플리케이션에 포함된 문서 판독기(reader). " 웹 페이지 검색: 사용자가 방문한 모든 웹 페이지를 색인화하기 위해 개인 검색 엔진을 만들 수 있는 웹 브라우저 또는 프록시 서버. 이것을 사용하여 쉽게 페이지를 다시 방문할 수 있다. " 웹 사이트 검색: 웹 사이트를 검색할 수 있는 CGI 프로그램 " 내용 검색: 저장된 문서에서 특정 내용을 검색할 수 있는 애플리케이션. 내용 검색 기능은 문서 열기 대화상자에 통합될 수 있을 것이다. " 버전 관리 및 컨텐트 관리: 문서나 문서 버전을 색인화해서 쉽게 검색할 수 있는 문서 관리 시스템. " 뉴스 및 유선(wire) 서비스: 뉴스가 도착했을 때 기사를 색인할 수 있는 뉴스 서버나 릴레이 서버.
< 그림1 > Lucene.Net 으로 개발된 Microsoft의 Lookout 을 설치한 outlook 화면
3. 인덱싱과 검색의 Core 클래스 이제 슬슬 본론으로 들어가 Lucene 검색 엔진을 살펴 보자. Lucene을 요리하기 위해 필요한 재료인 라이브러리와 api 는 http://lucene.apache.org 에서 구할 수 있다. 문서를 인덱싱 하고 검색하기 위해 필요한 핵심 클래스와 절차는 다음과 같다.
<인덱싱 요소> IndexWriter : 인덱스 파일을 생성하거나 수정(혹은 문서추가)하는 사용되는 클래스 Directory : 인덱스 파일이 저장될 경로를 담는 클래스 이다. IndexWriter 객체의 생성자의 인자로 사용된다. Analyzer : 문서를 인덱싱 하는 과정에서 다양한 형태로 token을 분리하는 역할을 한다. 역시 IndexWriter 객체의 생성자의 인자로 사용된다. Document : Field의 조합으로 이루어진 하나의 문서. 데이터베이스 에서 여러 column으로 이루어진 1건의 row 와 비슷한 개념이다. Field : Document를 구성하는 단위. 데이터베이스에서 하나의 column과 비슷한 개념이다.
위에서 나열한 요소를 가지고 문서를 인덱싱 하기 위해서는 다음과 같은 순서를 따른다. (1) 인덱스 파일이 저장될 경로 정보를 담는 Directory 객체 생성 (2) 인덱스 요소 분석을 위한 Analyzer 객체 생성 (3) Directory와 Analyzer 를 생성자의 인자로 IndexWriter 객체 생성 (4) Document 객체 생성 후 Document 객체에 필드 추가 (5) IndexWriter 객체에 Document 추가
<검색 요소> Searcher : 인덱스 파일을 read-only 모드로 열어서 검색하고 결과를 반환한다. Term : 검색의 기본 단위가 되는 클래스이며, 데이터 베이스 의 질의시 where name='maso' 와 같이 String 요소의 쌍으로 구성되어 있다. Query : 특정한 검색 포맷을 제공하는 클래스이다. 여러 구현체를 통해 다양한 검색 방법을 제공한다. TermQuery : Lucene이 제공하는 가장 일반적인 Query 클래스 이다. Hits : 검색결과를 담는 컨테이너 역할을 한다.
검색 절차 (1) 인덱스 디렉토리 경로를 인자 값으로 해서 Searcher 객체 생성 (2) 인덱스 요소 분석을 위한 Analyzer 객체 생성 (3) 검색을 위한 Query 객체 생성 (4) Searcher 객체의 search(Query query) 메쏘드를 호출하여 검색
4. 문서 인덱싱 및 검색 예제
<리스트1> 인덱싱 예제 (SimpleIndex.java)
package maso.lucene.indexing;
import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.analysis.WhitespaceAnalyzer; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field;
import java.io.IOException;
public class SimpleIndex {
private void index() throws IOException { String dirPath = System.getProperty("java.io.tmpdir", "tmp") + System.getProperty("file.separator") + "simple-index";
Directory dir = FSDirectory.getDirectory(dirPath, true); Analyzer analyzer=new WhitespaceAnalyzer(); ---------------(1) IndexWriter writer = new IndexWriter(dir, analyzer, true);
for (int i = 0; i < 10; i++) { Document doc = new Document(); doc.add(Field.Text("title", "Lucene 검색 엔진")); doc.add(Field.Text("content", "Lucene 의 소개 및 간단한 예제를 다룬다. ")); writer.addDocument(doc); } writer.optimize(); writer.close(); }
public static void main(String[] args) throws IOException { SimpleIndex si = new SimpleIndex(); si.index(); }
}
<리스트1>의 예제 코드는 인덱싱 작업을 하는 심플한 자바 프로그램 코드 이다. 지면상 핵심 코드만 추출하였지만 실행 가능한 전체 소스는 '이달의 디스크' 에서 찾아 볼수 있다. 이 예제에서 보는 바와 같이 Lucene을 이용해 인덱싱 하는 작업은 너무나도 간단하다. 먼저 인덱스 파일이 생성될 위치 정보를 담고 있는 Directory 객체와 Text 분석을 위한 Analyzer 객체를 생성 하고 이 두 객체를 생성자의 인자로 가지는 IndexWriter 객체를 이용해 Document를 담기만 하면 되는 것이다. 여기서 눈 여겨 볼 곳은 (1)번 표기가 된 Line의 Analyer 객체의 생성 부분이다. Lucene은 기본적으로 4개의 Built-in Analyzer 를 제공 하는데 이 예제에서 사용된 WhitespaceAnalyzer 와 StopAnalyzer, SimpleAnalyzer, StandardAnalyzer 등이 있다. 각각의 용도 및 특징은 다음 단원에서 좀더 세부적으로 알아보도록 하고, 여기에서 사용된 WhitespaceAnalyzer 가 공백 단위로 텍스트를 파싱 한다는 것 정도만 알고 넘어가자. 마지막으로 IndexWriter를 이용해 Document 를 저장한 후 프로그램을 반드시 호출해 줘야 하는 메소드가 있는데 예제 샘플의 마지막 두 라인에서 와 같이 optimize() 메쏘드와 close() 메소드가 있다. Close() 메쏘드는 index 파일의 변경된 내용을 적용시키고 관련된 모든 파일을 닫는다. 그리고 optimize() 메소드는 생성된 여러 인덱스 요소들을 하나로 묶는 기능을 수행한다. Optimize() 부분은 인덱스 튜닝과 연관되어 복잡하고 많은 내용을 담고 있으므로 다음 연재 에서 좀더 상세하게 다룰 것이다. '이달의 디스크'에서 전체 소스를 받아서 실행시켜 보면 시스템의 Temp 디렉토리( 일반적으로 C:\tmp )에 인덱스 파일이 생성되는 것을 볼 수 있을것이다.
<리스트2> 인덱스 검색 예제 (SimpleSearcher.java)
package maso.lucene.searching;
import org.apache.lucene.search.Hits; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.Searcher; import org.apache.lucene.analysis.SimpleAnalyzer; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.queryParser.ParseException;
import java.io.IOException;
public class SimpleSearch {
public Hits search() throws IOException, ParseException {
String dirPath = System.getProperty("java.io.tmpdir", "tmp") + System.getProperty("file.separator") + "simple-index";
Searcher searcher = new IndexSearcher(dirPath); Analyzer analyzer=new SimpleAnalyzer();
String queryString="검색"; String defaultField="title"; Query query = QueryParser.parse(queryString,defaultField,analyzer); ----------(1)
System.out.println("query=="+query.toString()); Hits hits= searcher.search(query); ---------------------(2)
searcher.close(); return hits;
}
public static void main(String[] args) throws IOException, ParseException {
SimpleSearch ss=new SimpleSearch(); Hits hits=ss.search(); int ctn=hits.length();
System.out.println(ctn+"건의 문서가 검색 되었습니다.\n"); for(int i=0;i<ctn;i++){ System.out.println(i+"번째 : "+hits.doc(i).get("title")); }
} }
<리스트 2> 는 <리스트1>과 마찬가지로 인덱스 파일을 검색하는 자바 프로그램의 핵심 코드만 추출한 예제 이다. 검색 과정을 살펴 보면 먼저 인덱스 파일 경로 정보를 가진 IndexSearcher 객체를 생성하고 (Searcher 클래스는 IndexSearcher 와 MultiSearcher, ParallelMultiSearcher 등이 있다.) (1) 표기 가 있는 라인 에서와 같이 검색할 query를 생성해서 (2)표기 라인에서 Hits 객체에 query 검색 결과를 담는다. (1)번 라인에서 QueryParser 에 의해 생성된 query는 Query 객체의 toString() 메쏘드를 호출하면 출력해볼수 있는데 query= ' title: 검색' 과 같은 단순한 구조 이다. 사족을 덧붙일 필요도 없겠지만 title 필드에서 '검색' 이라는 단어를 포함하는 문서를 찾는다 라는 의미 이다. 검색을 위해 필요한 핵심 클래스인 Searcher 와 Query 에 대한 내용도 다음 연재에서 보다 상세하게 다룰 것이다. 인덱싱 예제와 마찬가지로 검색 프로그램 에서도 반드시 Searcher 객체를 close 시켜야 하지만 Searcher 객체는 스레드에 안전하므로 성능을 위해서 오브젝트 풀링 기법을 사용하거나 싱글톤 패턴을 적용해 close 시키지 않고 재사용 하는 것도 무방하다. <리스트2> 예제에서는 마지막 라인에서 Hits 객체를 return 시키고 끝나는데 '이달의 디스크'를 통해 전체 소스를 받아 보면 반환된 Hits 객체에서 검색된 문서의 수와 검색결과를 출력하는 예제를 볼 수 있을 것이다.
<부가설명2>---------------------------------------------------------------------------------------------- 오브젝트 풀링과 싱글톤 패턴에 대한 내용은 최범균님의 javacan 사이트에 방문 하시면 잘 정리된 기사를 찾아 보실 수 있습니다. 오브젝트 풀링 : http://javacan.madvirus.net/main/content/contentRead.jsp?contentNo=7&block=4 싱글톤 패턴 : http://javacan.madvirus.net/main/content/contentRead.jsp?contentNo=5 ----------------------------------------------------------------------------------------------------------
5. Lucene 의 built-in Analyzer
Luene에서 Analyzer는 크게 2가지 용도로 사용되는데 첫번째는 인덱싱 작업에서 문서를 필드 형태로 나누는데 사용이 되며, 두번째 용도로는 검색시 쿼리를 파싱하는데 사용된다. Lucene에서 이미 만들어진 4가지 built-in Analyzer를 제공 하는데 WhitespaceAnalyzer, StopAnalyzer , SimpleAnalyzer 그리고 StandardAnalyzer 등이 있다. 아래 <표2>을 통해 각각의 특징에 대해서 살펴 보자.
Analyzer |
특징 |
WhitespaceAnalyzer |
스페이스를 구분으로 token을 분리한다.
WhitespaceTokenizer 사용 |
SimpleAnalyzer |
Letter를 구분으로 token을 분리한다.
LetterTokenizer 와 LowerCaseFilter 사용 |
StopAnalyzer |
Letter를 구분으로 token을 분리하고 , 중지단어를 token에서 제거한다.
LetterTokenizer , LowerCaseFilter , StopFilter 사용 |
StandardAnalyzer |
다양한 문법기반으로 token을 분리 가능하지만 비영어권 문자는 그대로 사용할 수 없고 커스터마이징이 필요하다.
StandardTokenizer, StandardFilter , LowerCaseFilter , StopFilter 사용
| <표 2> Lucene의 built-in Analyzer
다음은 "AB&C 한글 aaa@gmail.com" 이라는 문장을 <표2>에 나온 각각의 Analzer로 인덱싱 한 결과 이다.
a. WhitespaceAnalyzer : [AB&C] [한글] [aaa@gmail.com] b. SimpleAnalyzer : [ab] [c] [com] [gmail] [한글] [aaa] c. StopAnalyzer : [ab] [c] [com] [gmail] [한글] [aaa] d. StandardAnalyzer : [ab&c] [aaa@gmail.com]
그럼 이제 각각의 Analyzer에 대해 하나씩 알아보자. 먼저 WhitespaceAnalyzer 는 lucene의 4가지 built-in Analyzer 중 가장 심플한 Anaylzer 로서 단지 스페이스 단위로 token을 분리한다. 그 다음 SimpleAnalyzer는 Letter 단위로 문자를 나누기 때문에 공백이나 물론 특수문자는 제외되고 가장 많은 token으로 분리되며, LowerCaseFilter를 사용하므로 대문자는 모두 소문자로 변환된다. 나누어진 token 결과가 다른 Analyzer 보다 많으므로 인덱싱후 index파일의 크기 역시 가장 클 것이다. 그리고 다음 StopAnalyzer는 기본적으로 SimpleAnalyzer와 동일한 기능을 가지기 때문에 결과값 역시 동일하다. 다만 StopFilter를 사용해서 검색에 제외될 항목들 가령 and, an, if, else 같은 특정한 항목들을 지정해서 제외 시킬 수 있으므로 인덱싱이나 검색에 소요되는 시간과 인덱싱 파일의 용량을 효율적으로 줄 일수 있다. 마지막으로 StandardAnalyzer 가 있는데 다양한 문법 기반 하에 토큰을 분리하는 데다 StopAnalyzer와 같이 StopFilter를 사용하므로 다른 built-in Analyzer에 비해 가장 기능이 뛰어난 Analyzer 라고 볼 수 있다. 하지만 위의 인덱싱 결과에서 처럼 아쉽지만 한글과 같은 비 영어권 문자는 기본적으로 인식하지 못한다. 하지만 StandardAnalyzer가 기본적으로 사용하는 StandardTokenizer의 소스를 수정하거나 Lucene의 SandBox에 위치한 CJKAnalyzer(org.apache.lucene.analysis.cjk.CJKAnalyzer)를 사용하면 충분히 처리가 가능하다. ( SandBox : http://lucene.apache.org/java/docs/lucene-sandbox/ )
6. Lucene 관련 유용한 유틸리티 이번 단원에서 소개할 내용은 lucene 관련 유용한 third-party 유틸리티 이다.
Luke 가장 먼저 소개할 유틸리티는 아래 <그림2>에 나와 있는 인덱스 브라우저 Luke 이다. Lucene은 이진파일로 된 index파일을 사용하므로 언어에 관계없이 index파일을 읽을 수 있는데 이 luke 라는 유틸리티를 사용하면 마치 데이터베이스 관련 GUI 툴을 보듯 index파일의 내용을 일목요연 하게 볼 수 있으며 검색 기능도 제공한다. (http://www.getopt.org/luke/ 에서 찾아볼수 있다.)
<그림2> 인덱스 브라우저 Luke
Lucli Lucli는 Dror Matalon에 의해 배포되고 있는 Lucene의 Command Line 인터페이스 이다. 문서를 인덱싱 하기 위해 굳이 코드를 작성하지 않더라도 이 Lucli 를 사용하면 쉽게 문서의 인덱싱이 가능하다. Lucene의 SandBox에서 구할수 있으며, 관련 jar파일을 클래스 패스로 지정한 후 $JAVA_HOME/bin/java lucli.Lucli 명령을 실행하면 된다. 아직은 작성된 Document 도 없으며, 인덱싱 시에 StandardAnalyzer를 사용하도록 하드 코딩 되어 있으므로 실제 사용 시에 약간의 제약은 따른다.
Limo Limo는 Julien Nioche가 개발한 lucene Index Monitor 이다. http://limo.sourceforge.net 에서 다운로드 받을 수 있으며 limo.war 파일을 ServletContainer 에 올려서 바로 사용 가능하다. 톰캣의 경우엔 $TOMCAT_HOME/webapps/ 폴더에 .war 파일을 복사한다. 이제 서버를 실행하고 limo Application을 웹브라우저로 실행시켜 보자. Index 파일의 경로를 지정하는 폼 화면이 나온 후 경로를 적당히 지정해주면 아래 <그림3> 과 같은 화사한 웹 화면을 감상 하실 수 있을 것 이다. 물론 기본적으로 한글이 깨져서 나올 테지만 jsp 상단의 contentType 부분에 "charset=euc-kr" 을 추가해주면 한글 출력도 문제 없다. ( <%@page contentType="text/html;charset=euc-kr"%>) Limo는 Index 파일의 정보를 일목요연 하게 보여주며, 부가적으로 인덱스 파일의 검색 기능도 제공한다. Luke와 비교해 각각 일장 일단이 있으므로 각각 한번씩 비교해 보기 바란다.
<그림 3> Limo Application의 실행 화면
5. 결론 이번 연재 에서는 Lucene의 실전 활용 보다는 멋진 오픈 소스 검색엔진의 소개가 주 목적 이였기에 복잡한 내용은 최대한 배제하고 소개 글과 함께 기본 기능에 대해서만 간략하게 다루어 보았다. 다음 연재 에서는 Analyzer 의 보다 상세한 내용과 인덱스 튜닝에 대해 다루어 볼 예정이며 고급 검색 기법과 실전에 쓰일 만한 여러 가지 문서 포맷의 인덱싱 그리고 실제 Lucene의 적용시 격게 되는 여러 가지 문제점과 해결책에 대해 보다 심도 있게 다루어 보도록 하겠다. 지면상 광범위한 내용 전체를 전부 다룰 수가 없으므로 조금 아쉬움이 남기는 한다. 이번 기사에 부족함을 느끼는 독자들은 Apache Lucene 웹사이트와 wiki를 방문하면 다양한 레퍼런스를 포함해서 좋은 정보를 많이 얻을 수 있을 것 이다.
참고문헌 Lucene In Action ( manning 출판사 ) Doug Cutting 세미나 자료 : http://lucene.sourceforge.net/talks/inktomi/ 자바월드 기사 : http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-lucene_p.html
|