[Lucene] java.lang.OutOfMemoryError : Java heap space

검색엔진로그 2011. 3. 15. 12:43



루씬을 사용하는 도중  Searcher 클래스에서 다음과 같은 에러가 발생했다. 

java.lang.OutOfMemoryError: Java heap space
        at org.apache.lucene.index.SegmentReader$Norm.bytes(SegmentReader.java:463)
        at org.apache.lucene.index.SegmentReader.getNorms(SegmentReader.java:1017)
        at org.apache.lucene.index.SegmentReader.norms(SegmentReader.java:1024)
        at org.apache.lucene.search.TermQuery$TermWeight.scorer(TermQuery.java:79)
        at org.apache.lucene.search.IndexSearcher.search(IndexSearcher.java:210)
        at org.apache.lucene.search.Searcher.search(Searcher.java:67)

 색인파일이 31G 정도 쌓였을 쯤 발생한 에러인데,  색인이 너무 커서, 검색도중  메모리가 부족해지는 현상이었다.  
임시방편으로 
set  JAVA_OPTS="-Xms512m -Xmx1024m"

명령으로  JVM의 힙메모리 크기를 늘려주었더니 뻣는 현상은 해결이 되었다.  하지만 색인 파일은 
자료가  쌓일수록 더 커질 것이기 때문에 언젠가 다시 메모리 부족현상이 발생하게 될 것이다.  
좀더 근본적인 해결책을 찾아봐야 겠다. 

현재는 다수의 램디렉토리와 단일 파일디렉토리를 사용하여 색인을 수행하고 있다.  즉 하나의 파일 색인기를 사용하고 있는데 
이 파일 색인기를 하나만 사용할 것이 아니라  다중으로 분리하여 구성하면 위 문제에 대한 해결책이 어느정도 될 것 같다. 
이렇게 하면 합너에 읽어들이는 색인파일의 크기도 줄어들 것이고, 최적화도 색인기별로 별도로 수행할 수 있으며, 
하나의 색인기가 에러가 발생하더라도 나머지 색인기를 사용할 수 있기 때문에 큰 장점이 된다. 
다중 색인기에서 동시에 검색을 지원하는 루씬 클래스도 지원되기 때문에 검색모듈을 구축하는 부분도 크게 달라지지
않는다.  

루씬과 색인,검색 클래스 사용예(Java)

검색엔진로그 2011. 3. 8. 00:41


 

인덱싱 클래스 ATIndexer

인덱싱 데이터는 트위터에서 수집한 status 데이터를 별도 저장한 텍스트 파일을 사용하였으며, 

한건의 트윗글에 대해  status_id(글번호), text(내용), user_id(사용자번호), user_name(사용자이름)  

데이터만 사용하였다. 

루씬이 제공하는  필드에 따라 인덱싱하는 방법을 표현하기 각 데이터에 대해 다음과 같은 필드생성 옵션을 주었다. 

 

 

status_id : Field.UnStored 를 사용. 분석과 검색을 수행하지만, 저장되지 않기 때문에 검색결과에서 보여줄 수는 없음 

text : Field.Text 를 사용. 분석과 검색, 저장을 허용하여, 검색결과로 원문을 보여줄 수 있다. 
user_id : Field.UnIndexed 를 사용. 검색어로 사용할 수 없지만, 다른 필드의 검색결과에서 같이 보여질 수 있다. 

user_name : Field.Keyword 를 사용. 분석을 하지 않지만, 검색과 저장을 한다. 검색시 이 필드는 분석되면 않되기 

때문에 직접 텀쿼리를 만들어 IndexSearcher의 search 메소드에 전달해야한다. 

 

인데싱에 사용한 파일(crawled_text.data)의 포맷  

...

status_id:2147483648
text:@NicoleLapin so many buttons
user_id:12
user_name:Jack Dorsey

status_id:2147483648
text:In alaska for the longest day of the year with Delphine and eWee. Seaplanes. Sunshine.
user_id:14
user_name:noah glass

...

 

소스ATIndexer.java

      lucene-1.4.3.jar

       log4j-1.2.8.jar

  

package net.game.lucene.study;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RangeQuery;
import org.apache.lucene.search.TermQuery;

// IndexSearcher
// Term
// Query
// TermQuery, RangeQuery, PrefixQuery, BooleanQuery, PharaseQuery, WildcardQuery, FuzzyQuery 
// Hits

public class ATSearcher {
        
        private String indexDir;
        private IndexSearcher indexSearcher;
        private Analyzer analyzer;
        
        private Logger log = Logger.getLogger(ATSearcher.class);
        
        public ATSearcher(String indexDir, Analyzer analyzer) throws IOException {
                BasicConfigurator.configure();
                
                this.indexDir = indexDir;
                this.analyzer = analyzer;
                this.indexSearcher = new IndexSearcher(this.indexDir);
        }
        
        public Hits search(String field, String searchWord) throws Exception {
                Hits hits;
                // user_name필드는 직접 텀쿼리를 생성하여 전달해야 올바른 검색결과를 얻을 수 있음 
                if (field.equals("user_name")) {
                        hits = this.indexSearcher.search(new TermQuery(new Term(field, searchWord)));
                } else {
                        Query query = QueryParser.parse(searchWord, field, this.analyzer);
                        hits = this.indexSearcher.search(query);
                }
                
                return hits;
        }
        
        public static void main(String[] args) throws Exception {
                BasicConfigurator.configure();
                
                ATSearcher searcher = new ATSearcher("/Users/hiddenviewer/index_lucene", new StandardAnalyzer());
                
                String line;
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                
                System.out.println("<검색필드:검색어> 입력! (종료: quit)");
                
                while ((line = br.readLine()) !=  null) {
                        if (line.startsWith("quit")) {
                                System.out.println("ATIndexer Test 프로그램 종료!");
                                break;
                        } 
                        int delimIndex = line.indexOf(":");
                        if (delimIndex == -1) {
                                System.out.println("<검색필드:검색어> 포맷으로 입력해주세요");
                                continue;
                        }
                        
                        Hits hits = searcher.search(line.substring(0, delimIndex), line.substring(delimIndex+1));
                        
                        System.out.println("------------------------------------------");
                        System.out.format("검색결과 개수: %d \n", hits.length());
                        System.out.println("------------------------------------------");
                        
                        for (int i = 0; i < hits.length(); i++) {
                                Document d = hits.doc(i);
                                System.out.format("%20s: %s \n", "status_id", d.get("status_id"));
                                System.out.format("%20s: %s \n", "text", d.get("text"));
                                System.out.format("%20s: %s \n", "user_id", d.get("user_id"));
                                System.out.format("%20s: %s \n", "user_name", d.get("user_name"));
                        }
                }
                
        }
}

 


검색 클래스 ATSearcher

ATIndexer 가 인덱싱한 색인파일에서 검색을 하기 위해서 필드명:검색어 형태로 입력을 받는다. 필드명에 해당하는 필드에서

검색어가 포함된 도큐먼트를 찾아 검색결과를 출력해준다. 이때 중요한 것은 인덱싱 과정에서 Field.UnIndexed 로 지정한 user_id 필드는

검색어를 통해 검색할 수 없으며, 다른 필드를 통한 검색결과에만 포함되어 나온다. 또 Field.UnStored로 지정한 status_id 는 글번호로 검색은
가능하지만, 원문을 저장하고 있지 않기 때문에 결과에는 원문이 출력되지 않는다. Field.Keyword 로 지정한 user_name은 인덱싱과정에서

분석이 되지 않기 때문에, 검색과정에서도 분석기에 의해서 분석되면 안된다. 검색필드가 user_name 인 경우에는 텀쿼리를 직접생성하여

IndexSearcher의 search 메소드에 전달해줘야 올바른 결과를 얻을 수 있다.

 

 

소스 : ATSearcher.java

 

package net.game.lucene.study;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RangeQuery;
import org.apache.lucene.search.TermQuery;

// IndexSearcher
// Term
// Query
// TermQuery, RangeQuery, PrefixQuery, BooleanQuery, PharaseQuery, WildcardQuery, FuzzyQuery 
// Hits

public class ATSearcher {
        
        private String indexDir;
        private IndexSearcher indexSearcher;
        private Analyzer analyzer;
        
        private Logger log = Logger.getLogger(ATSearcher.class);
        
        public ATSearcher(String indexDir, Analyzer analyzer) throws IOException {
                BasicConfigurator.configure();
                
                this.indexDir = indexDir;
                this.analyzer = analyzer;
                this.indexSearcher = new IndexSearcher(this.indexDir);
        }
        
        public Hits search(String field, String searchWord) throws Exception {
                Hits hits;
                // user_name필드는 직접 텀쿼리를 생성하여 전달해야 올바른 검색결과를 얻을 수 있음 
                if (field.equals("user_name")) {
                        hits = this.indexSearcher.search(new TermQuery(new Term(field, searchWord)));
                } else {
                        Query query = QueryParser.parse(searchWord, field, this.analyzer);
                        hits = this.indexSearcher.search(query);
                }
                
                return hits;
        }
        
        public static void main(String[] args) throws Exception {
                BasicConfigurator.configure();
                
                ATSearcher searcher = new ATSearcher("/Users/hiddenviewer/index_lucene", new StandardAnalyzer());
                
                String line;
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                
                System.out.println("<검색필드:검색어> 입력! (종료: quit)");
                
                while ((line = br.readLine()) !=  null) {
                        if (line.startsWith("quit")) {
                                System.out.println("ATIndexer Test 프로그램 종료!");
                                break;
                        } 
                        int delimIndex = line.indexOf(":");
                        if (delimIndex == -1) {
                                System.out.println("<검색필드:검색어> 포맷으로 입력해주세요");
                                continue;
                        }
                        
                        Hits hits = searcher.search(line.substring(0, delimIndex), line.substring(delimIndex+1));
                        
                        System.out.println("------------------------------------------");
                        System.out.format("검색결과 개수: %d \n", hits.length());
                        System.out.println("------------------------------------------");
                        
                        for (int i = 0; i < hits.length(); i++) {
                                Document d = hits.doc(i);
                                System.out.format("%20s: %s \n", "status_id", d.get("status_id"));
                                System.out.format("%20s: %s \n", "text", d.get("text"));
                                System.out.format("%20s: %s \n", "user_id", d.get("user_id"));
                                System.out.format("%20s: %s \n", "user_name", d.get("user_name"));
                        }
                }
                
        }
}


ATSearcher 실행결과

결과를 보면 Field.UnStored로 지정된 필드 status_id는 원문이 출력되지 않는 걸 알수 잇다.






색인과 검색 주요 클래스

검색엔진로그 2011. 3. 8. 00:27


 Apache OpenSource Project인 Lucene 을 구성하는 색인과 검색과 관련된 주요 클래스를 알아보자. 

 

색인 주요 클래스

색인과정 중에 사용되는 주요 클래스는 다음과 같다. 

클래스 설명
IndexWriter 색인을 생성하고, 색인에 도큐먼트를 추가한다.
IndexReader 색인에서 도큐먼트를 삭제한다
Directory

색인의 저장소. 일반적으로 파일시스템 FSDirectory와 메모리 RAMDirectory가 있다. 루씬 샌드박스에서 제공하는

데이터베이스를 색인의 저장소로 사용하는 JDBCDirectory 도 있다. 

Analyzer

내용을 텀 단위로 분리하는 역할을 하며, 색인과 검색 단계에서 동일한 분석기가 적용되야 올바른 검색결과를 얻을 수 있다. 

Document

검색을 위한 정보 구성의 단위가 된다. 하나의 메일 메시지 또는 하나의 웹페이지가 도큐먼트가 될수 있으며, 비지니스 로직에

따라 변경된다.

Field

도큐먼트 내에서 정보를 검색하는 대상(카테고리)이다. 분석의 여부, 검색 가능여부, 저장하는지 여부에 따라 Keyword, UnIndexed, UnStored, Text 필드로 구분된다.  이러한 구분의 검색과 검색결과로 보여질 데이터의 항목들을 명확히 하여,

검색의 질과 효율을 높이기 위함이다.


색인은 양질의 검색결과를 얻을 수 있도록 하는 근본이 되는 과정이다.  도큐먼트는 색인과 검색과정에서 정보의 저장과 검색의 단위가 되기 때문에, 어떠한 데이터들을 하나의 도큐먼트로 구성할지가 중요한 요소가 된다. 

 

도규먼트를 구성하는 필드들은 다음과 같은 값들 일 수 있다. 

필드 분석 검색 저장 사용 용도
Field.Keyword X O O  변경되지 않는 고유정보 저장하기 위해 사용됨. 예) 파일의 경로, 메일번호, 게시물 번호 
Field.UnIndexed X X O  검색의 결과로 보여주기 위해서만 사용되는 정보. 예) 웹페이지 글의 상위 네번째줄까지  글....
Field.UnStored O O X  검색의 대상이 되지만, 화면에는 보여질 필요가 없는 정보. 예) 웹문서의 태그...
Field.Text O O

String(O)

Reader(X)

String  타입은 내용이 색인에 저장되고, Reader 타입은 내용이 색인에 저장되지 않는데,  Reader는

불필요하게 많은 데이터를 저장하여 효율을 저하시킬 수 있기때문에 저장하지 않는다.

 

 

검색 주요 클래스 

검색과정 중에 사용되는 주요 클래스는 다음과 같다. 

 

클래스 설명
IndexSearcher

 색인에서 검색어를 찾아 결과를 반환하는 클래스.  검색단계에서도 검색어를 분석하는데 Analyzer 가 사용된다.

  이때 색인단계와 검색단계에서 동일한 텀이 생성되게 하는 것은 루씬 라이브러리를 사용하는 개발자의 몫이다. 

Term  매칭이 되는 가장 최소단위의 단어이다. 색인과 검색과정에서 (필드이름, 단어) 의 쌍으로 구성된다. 
Query

 루씬엔진이 해석할 수 있도록  검색의 조건을 변환한 클래스이다.  TermQuery, RangeQuery, PrefixQuery 등 다양한 조건에

 해당하는 Query문이 존재한다.  개발자는 사용자에게 적절한 검색인터페이스를 제공하여,  올바른 Query 을 생성해야한다 

QueryParser  다양한 조건을 좀더 편리하게 문자열 조건문 형태로 입력받고, 이 문자열을 Query 서브클래스들의 조합으로 변환한다.  
Hits

 IndexSearcher 클래스의 search 메소드의 검색결과 매칭된 도큐먼트들을 가지고 있는 결과객체이다. 실제 문서의 내용이

아닌 문서에 대한 ID 만을 가지고 있어서, 실제로 요청할 때 해당 도큐먼트를 디스크에서 로드해서  반환한다.