BLOG ARTICLE 루씬 | 2 ARTICLE FOUND

  1. 2008/07/02 루씬 고속 색인 방법 : 토큰을 재사용하라 (3)
  2. 2008/06/27 이클립스로 루씬 소스 다루기 (2)

사용자 삽입 이미지

"버스 토큰"
from 네이버 기차여행카페 - 바이트레인


오픈소스를 가져다 쓸다면 "유지보수와 튜닝 문제"에 골치 아파질 때가 많다. 오픈소스 덕분에 빠르게 시작할 수 있지만 내부에 대한 이해 부족으로 해법을 찾기 어렵기 때문이다. 혹자는 오픈소스에는 개발자들이 많아 질문만 던지면 즉시 해결될 것이란 유언비어에 빠지는 분도 있는데, 실무에서 만나게 되는 시급한 문제에 대해서는 즉시 응대와 해결은 꿈도 꾸질 말아야 한다. 특히 국내 실정은 더하다.

루씬도 역시 오픈소스인 것 같다. 루씬 2.3.2 소스를 보고 있는데 색인어 추출 루틴들인 Analyzer 계열 클래스들의 복잡도가 만만치 않다. Payload란 실험적인 개념, PositionIncrement 개념, 수많은 클래스들 등이 주석과 함께 코드에 뒤범벅이 되어 있어 웬만한 내공이 아니면 이해하기 어렵다.

내부 이해야 믿으면 그만이지만 검색엔진 적용 시에 대용량 문서를 접하게 되면 속도 튜닝이 아주 힘들다. 루씬 색인속도 향상에 관해서는 Job tech blog를 한번 읽어보라. 이 블로그에 나와있는 5가지 팁 중에 4번째에 이 글 주제인 토큰 재사용에 관한 언급이 있다(나머지 팁들도 유용하다.)

루씬에서는 색인어를 토큰(Token)이라 부른다. 검색엔진의 색인속도에 영향을 주는 여러 요인들이 있지만 토큰을 추출할 때 마다 토큰 문자열을 매번 재 생성하지 않고 토큰 버퍼(Token Buffer)를 만들어 재사용하는 방식으로 6-7배 이상의 속도 향상을 기대할 수 있다(전체 색인 속도 향상을 말하는 것은 아님에 주의하자.) 루씬도 2.3 버전부터 토큰 버퍼 개념을 적용하고 있다. 토큰 버퍼를 사용하면 추출속도를 높일 수는 있으나 다음 추출 시에 토큰버퍼 내용이 지워지므로 사용법에 주의해야 한다. 아래 실험코드가 있다.

GeSHi © 2004, Nigel McNie
  1. import java.io.BufferedReader;
  2. import java.io.IOException;
  3. import java.io.InputStreamReader;
  4. import java.io.StringReader;
  5. import java.net.URL;
  6.  
  7. import org.apache.lucene.analysis.Token;
  8. import org.apache.lucene.analysis.TokenStream;
  9. import org.apache.lucene.analysis.standard.StandardAnalyzer;
  10.  
  11. enum Interface {
  12.   OLD_INTERFACE, NEW_INTERFACE
  13. }
  14.  
  15. public class ReusableTokenTest {
  16.   int textLength = 0;
  17.   int numTokens = 0;
  18.   long elapsedTime = 0;
  19.  
  20.   public void tokenInterface(Interface which) {
  21.     try {
  22.       StringReader reader = new StringReader(getLargeString());
  23.       TokenStream tokens = new StandardAnalyzer().tokenStream(new String(
  24.           "text"), reader);
  25.       long startTime = System.currentTimeMillis();
  26.       if (which == Interface.OLD_INTERFACE)
  27.         this.useOldInterface(tokens);
  28.       else
  29.         this.useNewInterface(tokens);
  30.       this.elapsedTime = System.currentTimeMillis() - startTime;
  31.       this.output(which);
  32.     } catch (Exception e) {
  33.       e.printStackTrace();
  34.     }
  35.   }
  36.  
  37.   private void output(Interface which) {
  38.     StringBuffer sb = new StringBuffer();
  39.     if (which == Interface.OLD_INTERFACE)
  40.       sb.append("Old Interface");
  41.     else
  42.       sb.append("New Interface");
  43.     sb.append("\t");
  44.     sb.append(this.textLength);
  45.     sb.append(" (chars)\t");
  46.     sb.append(this.numTokens);
  47.     sb.append(" (tokens)\t");
  48.     sb.append(this.elapsedTime);
  49.     sb.append(" (msec)");
  50.     System.out.println(sb.toString());
  51.   }
  52.  
  53.   private String getLargeString() throws IOException {
  54.     URL url = new URL("http://insidesearch.tistory.com/");
  55.     BufferedReader in = new BufferedReader(new InputStreamReader(url
  56.         .openStream()));
  57.     StringBuffer sb = new StringBuffer();
  58.     String str;
  59.     while ((str = in.readLine()) != null) {
  60.       sb.append(str);
  61.     }
  62.     this.textLength = sb.length();
  63.     return sb.toString();
  64.   }
  65.  
  66.   private void useOldInterface(TokenStream tokens) throws IOException {
  67.     Token token;
  68.     while ((token = tokens.next()) != null) {
  69.       useToken(Interface.OLD_INTERFACE, token);
  70.     }
  71.   }
  72.  
  73.   private void useNewInterface(TokenStream tokens) throws IOException {
  74.     Token token = new Token();
  75.     while ((token = tokens.next(token)) != null) {
  76.       useToken(Interface.NEW_INTERFACE, token);
  77.     }
  78.   }
  79.  
  80.   private void useToken(Interface m, Token token) {
  81.     this.numTokens++;
  82.     if (m == Interface.NEW_INTERFACE) {
  83.       String s = new String(token.termBuffer(), 0, token.termLength());
  84.     }
  85.     else {
  86.       String s = token.termText();
  87.     }
  88.   }
  89.  
  90.   public static void main(String[] args) {
  91.     new ReusableTokenTest().tokenInterface(Interface.OLD_INTERFACE);
  92.     new ReusableTokenTest().tokenInterface(Interface.NEW_INTERFACE);
  93.   }
  94. }
  95.  
Parsed in 0.125 seconds


위 코드는 http://insidesearch.tistory.com에서 비교적 큰 문자열을 받아 StandardAnalyzer를 사용해 토큰을 만든다. 이 코드를 돌려보면 새로운 인터페이스를 사용하면 6~7배 정도 속도가 향상됨을 볼 수 있다(아래 결과 참조). 이 정도면 수억개의 토큰을 다루는 대용량 문서들을 색인할 때는 수 십분 가량의 속도 향상을 기대할 수 있을 것이다.

Old Interface 56012 (chars) 9105 (tokens) 39 (msec)
New Interface 56012 (chars) 9105 (tokens) 6 (msec)


요약하면 새로운 토큰 인터페이스를 사용하여 추출 속도를 높이려면 기 정의된 토큰 객체를 재사용한다는 점만 기억하면 된다. 내부 토큰버퍼 크기 재조정 작업은 알아서 Token 클래스 내부에서 척척 해 준다. 외부에서 getTermText()를 사용해 내부에서 만들어준 문자열 객체를 사용하지 말고  termBuffer()를 호출해 내부 토큰버퍼(char [])에서 termLength() 만큼 복사해서 사용하면 된다.

@webJOY

참고로 토큰버퍼 내부 동작원리에 대해서는 DEV 용식님이 블로그을 참고하라.



사용자 삽입 이미지


요즘 자바 매력에 푹 빠져있다. 아니 이클립스란 괴물의 매력에 빠져 있다는 것이 더 옳을 것 같다. 2주 정도 만지작거린 도구지만 지금까지 보아왔던 개발도구 중에 단연 으뜸인 것 같다. 있을 것 같다고 생각하면 진짜 그런 기능이 있는 소프트웨어가 좋은 소프트웨어라고 생각하는데 이 이클립스가 그렇다.

요즘 회사에서 이클립스를 가지고 파일럿 코딩을 하고 있다. 이에 부스팅받아 생계터전인 검색바닥도 다질겸 소스 배포판을 다운받아 이클립스 작업공간에 가져다 놓고 테스트 프로그램도 작성해보고 내부코드도 수정해가면서 배울려고 했는데 각자는 환상인데 궁합을 맞추려니 쉽지 않았다. 구글링을 해보아도 이클립스에 루씬소스를 올리고 컴파일하는 방법에 대해 나와 있지 않아 약간의 삽질 경험을 공유하려 한다. 루씬, 이클립스 둘다 초짜인데 눈 대중으로 작업한 결과다.

  1. 먼저 루씬 소스를 다운받아 원하는 폴더에 푼다. 이클립스가 소스를 Import하면 복사하기 때문에 아무 곳에 풀면 된다.
  2. 이클립스 File > New > Java Project 메뉴로 Jar 컴파일용 프로젝트(예, LuceneSource)를 만든다. 새로운 프로젝트가 만들어지면 이클립스는 자동으로 src란 폴더를 만든다. 이 폴더에 적절히 루씬 코드를 import해야 한다.
  3. 이클립스 File > Import 메뉴로 루씬소스의 특정 폴더를 읽어 들인다. 루씬 폴더를 무작정 import하면 namespace를 찾을 수 없어 이클립스의 친절한 빨간불들을 보게 된다. Import 메뉴를 클릭해서 나온 마법사에서 Import Source로 File System을 선택하고 Next를 클릭한다. 다음에 나타나는 마법사의 From Directory Browse 버튼을 클릭해서 루씬소스 코드의 lucene-2.3.2/src/java 폴더를 클릭해서 읽어 들인다. 아래 화면과 같이 설정되어야 한다. Import할 소스를 java > org > apache > lucene 순으로 선택하지말고 lucene를 직접 클릭해야 한다. 무슨 차이가 있는지는 모르겠다.
    사용자 삽입 이미지
  4. 이클립스 File > Export 메뉴로 Jar 파일을 특정 폴더에 lucene.jar 파일을 생성한다.
  5. 이클립스 File > New > Java Project 메뉴로 테스트용 프로젝트(예, LuceneStudy)를 만든다.
  6. 테스트 프로젝트의 Build Path > Add External Archives... 메뉴를 이용해 소스 프로젝트에서 생성했던 lucene.jar 파일을 찾아 선택한다.
  7. 테스트 프로젝트에 루씬 기능을 테스트할 Java Class를 만들고 실행해본다. 잘 동작하면 제대로 설정한 것이다.

위 7 단계를 통과하고 생성된 프로젝트 모습은 아래 그림과 같아야 한다. 루씬 소스 배포판에는 루씬 Jar 라이브러리를 위한 소스 폴더(src/java/)가 있고 테스트, 데모, 기타 폴더들이 있다. 루씬 Jar 라이브러리에는 소스 폴더만 필요하다. 이런 점 때문에 루씬 Jar 라이브러리 생성용 프로젝트와는 별도로 테스트 코드를 위한 프로젝트도 만들었다. 한 프로젝트 안에 넣으면 루씬 내부 코드와 테스트 코드를 동시에 수정하면서 작업이 가능하기 때문에 더 좋을 것 같다. 이 정도만 해도 디버깅을 할 때 루씬 내부코드까지 보면서 할 수 있으니 툴 삽질은 여기까지만 한다.

사용자 삽입 이미지
이클립스로 루씬 소스를 분석할 환경을 마련했다. 분석환경이 잘 도는지 확인도 할 겸 LuceneStudy 프로젝트에 StandardAnalyzer 기능을 테스트하는 Analyzer.java를 만들어 넣어보자. 아래와 같은 코드를 넣고 실행해보라.

GeSHi © 2004, Nigel McNie
  1. public class Analyzer {
  2.   public static void main(String[] args) {
  3.     StringReader reader = new StringReader(new String(
  4.         "jongwanyun@gmail.com 메일주소"));
  5.  
  6.     StandardAnalyzer analyzer = new StandardAnalyzer();
  7.     TokenStream tokens = analyzer.tokenStream(new String("email"), reader);
  8.     Token token = new Token();
  9.     try {
  10.       int tokenId = 0;
  11.       while ((token = tokens.next()) != null) {
  12.         StringBuffer out = new StringBuffer();
  13.         out.append(tokenId++);
  14.         out.append("\tToken: [");
  15.         out.append(token.termBuffer());
  16.         out.append("]\t");
  17.         out.append("Type/start/end/length: [");
  18.         out.append(token.type());
  19.         out.append(",");
  20.         out.append(token.startOffset());
  21.         out.append(",");
  22.         out.append(token.endOffset());
  23.         out.append(",");
  24.         out.append(token.termLength());
  25.         out.append("]");
  26.         System.out.println(out.toString());
  27.       }
  28.     } catch (IOException e) {
  29.       e.printStackTrace();
  30.     }
  31.   }
  32. }



@webJOY