목요일, 3월 28
Shadow

#022 색인 커스터마이징

1. Directory
루씬의 추상 클래스인 Directory 클래스는 간단한 파일 형식의 저장 공간을 나타내는 Directory 클래스는 간단한 파일 형식의 저장 공간을 나타내는 API이다. 루씬에서 색인파일을 읽거나 쓰려는 경우 항상 Dicionary의 메소드를 통해 처리한다.
간단하게 설명하면,
1. SimpleFsDirectory

색인 파일 시스템에 저장하는 가장 기본적인 Directory 클래스, java.io.*패키지의 기능을 활용하여, 동시에 사용하려는 쓰레드 개수가 많아지면 성능이 떨어진다. 

2. NIOFSDirectory

색인을 파일 시세템에 저장하지만 java.nio.* 패키지의 기능을 활용한다. 동시에 사용하려는 쓰레드의 개수가 많아져도 성능이 크게 떨어지지 않는다. 

3. MMapDirectory

메모리 맵 I/O 기능을 활용해 파일의 내용일 읽어온다. 

4. RAMDirectory

모든 파일을 메모리에 보관하는 Directory이다.

5. FileSwitchDirectory

두개의 디렉토리를 사용하여, 파일 확장자에 따라 두개의 디렉토리를 바꿔가며 사용한다. 

RamDirectory는 색인된 내용을 디스크 대신 메모리에 저장한다. 모든 색인을 메모리에 보관하면 읽기 쓰기 속도가 엄청나게 빨라지며, 따라서 색인을 메모리 안에 담을 수 있을 만큼 크기가 작고 원본 문서에서 언제든 재빨리 색인 할 수 있다.
루씬 매부적으로 자동 단위 테스트를 진행 할 때 테스트용 임시 색인을 저장할 공간으로 RAMDirectory를 많이 사용한다. RAMDirectory에 색인을 생성하려면 IndexWirter 인스턴스를 다음과 같이 생성하면 된다.

        RAMDirectory ramDirectory = new RAMDirectory();
 IndexWriter indexWriter = new IndexWriter(ramDirectory,
                new IndexWriterConfig(new WhitespaceAnalyzer())
                        .setOpenMode(IndexWriterConfig.OpenMode.CREATE));

IndexWriter인스턴스를 사용해 일반적으로 문서추가, 삭제, 변경 등의 작업을 진행할 수 있다. 다만 JVM이 종료되면 RAMDirectory에 담겨있던 색인도 한순간에 사라진다는 점의 주의하자.
현재 디스크에 저장되 있는 색인의 크기가 충분히 작은 경우 해당 색인을 모두 메모리에 불러와 검색 속도를 높이고자 할 때 유용한 방법이다. 하지만 앞서 언급한 것 처럼 대부분의 최신 운영체제는 여유 메모리를 활용해 디스크 I/O속도를 매우 빠르게 처리 할 수 있게 도와주기 떄문에 실제로 RAMDirectory를 사용할 때 엄청난 수준의 속도 향상이 일어나지는 않는다.
이와 비슷하게 원본 Directory에 담긴 내용을 다른 Directory에 복사하는 다음과 같은 메소드도 있다.

        RAMDirectory ramDirectory = new RAMDirectory();
        ramDirectory.copyFrom(Directory from, String src, String dest, IOContext context);

2. 병렬처리, 쓰레드 안정성, 락
루씬의 병렬처리 규칙은 매우 간단하다.

 특정 색인에 대해 읽기 전용의 IndexReader는 몇개라도 열어 사용할 수 있다. 이게 같은 장비이거나 다른장비에서 실행되더라도 상관없이 사용할수있다. 하지만 성능과 시스템 자원 활용의 측면에서 하나의 IndexReader인스턴스를 생성한 다음 여러 쓰레드에서 IndexReader를 공유해 사용하는 편이 좋다. 즉 여러 쓰레드에서 하나의 IndexReader인스턴스를 통해 검색 할 수 있다. 
색인 하나에 대해 IndexWriter는 하나만 열수 있다. 루씬에서 쓰기 락을 사용해 IndexWriter를 하마나 열게 제한한다. indexWriter인스턴스가 생성되자 마자 쓰기 락을 확보하며, 해당 IndexWriter를 닫아야 쓰기 락이 해제 된다. 
indexReader는 indexWriter가 색인의 내용을 변경하고 있는 도중이라도 언제든지 열어 사용할수 있다. IndexReader는 변경 작업과 관계없이 열리는 시점의 색인을 나타낸다. 따라서 IndexWriter가 진행한 변경 사항을 반영하고 IndexRedader를 새로 열기 전에는 변경 사항이 보이지 않는다. 
IndexReader나 IndexWriter인스턴스는 여러 쓰레드에서 얼마든지 공유해 사용해도 좋다. 

3. 원격파일 시스템의 색인 공유
여러대의 장비 여러개의 JVM에서 단 하나의 색인을 공유 하려면 해당 색인을 원격 파일 시스템으로 열어줘야 한다. 가장 쉽게 생각할 수있는 방법은 색인이 저장되는 파일 시스템과 같은 장비에서 IndexWriter를 실행해 색인의 내용을 변경할 수있게 하고, 여러대의 장비를 네트워크 파일 시스템으로 연결해 색인의 내용을 검색하게 하는 방법이다.
루씬은 색인에 접근할때 상식적인 수준에서 최대한의 병렬성을 지원한다. 여러 IndexReader가 하나의 색인을 공유 할 수도 있고 여러 쓰레드에서 하나의 IndexReader와 IndexWriter를 공유 할수도 있다. 중요한점은 하나의 색인을 대상으로 두개 이상의 IndexWriter를 사용할 수 없다는 것이다.

4. 색인 락
색인 하나에 하나의 IndexWriter만 접근할 수 있게 루씬은 파일 기반의 락을 사용한다. 색인 디렉토리 파일이 존재한다면 어떤 IndexWriter인스턴스가 이미 쓰기 권한을 쥐고 있다는 뜻이다. write.lock파일이 존재하는 상황에서 다른 IndexWriter를 생성하거나 IndexReader에서 변경 기능을 사용하려 하면 LockObtainFailedException이 발생한다. 동일한 색인을 대상으로 두개 이상의 IndedWriter인스턴스가 생성돼 색인이 깨지지 않게 막아주는 예외 상황이다.

1.NativeFSLockFactory

FSDirectory 클래스에서 사용하는 기본 락이며, java.nio패키지를 통해 운영체제에서 제공하는 락 기능을 활용한다. 

2. SimpleFSLockFactory

자바의 File.createNewFile 메소드를 사용해 락 파일을 생성하며, 따라서 NativeFsLockFactory보다 시스템의 종류에 영향을 덜 받는다. 하지만 JVM이 비정상적으로 종료되거나 JVM이 종료 할때 IndexWriter를 제대로 닫지 못한다면 write.lock파일이 남아있을수 있다. 파일이 남아 있을경우 수동으로 직접 제거해야 한다. 

4. SingleInstanceLockFactory

락을 메모리 안에 생성한다. RAMDirectory를 사용하는 경우 기본 설정으로 사용한다. 모든 IndexWriter가 동일한 JVM안에서 생성될떄만 제대로 사용할 수 있다. 

5. NoLockFactory

락을 사용하지 않는다. 루씬이 제공하는 락을 사용할 필요가 없다고 판단되는 경우에만 사용해야 한다. 

IndexWriter에서 락을 확보 했다면 그 이후에 생성하려는 IndexWriter는 단순하게 기본 설정으로 1초마다 락을 확보하려고 재시도 한다. 기존에 락을 갖고 있던 IndexWriter가 락을 해제하면 즉시 신청한 순서대로 다음 IndexWriter에 락을 넘겨주등의 큐는 없다.

IndexWriter클래스의 isLock(DIrectory)메소드는 지정한 Directory에 들어있는 색인에 쓰기 락이 걸려 있는 상태인지 확인한다 IndexWriter인스턴스를 새로 생성하기 전에 락이 걸려 있는 상태인지 확인하는등의 경우에 유용하다.  
IndexWriter클래스의 unlock(Directory)메소드는 지정한 Directory에 들어있는 색인에 락이 걸려 있는 경우 강제로 락을 해제한다. 

이제 루씬 내부적으로 쓰기 락을 사용한다는 점을 충분히 살펴 봤고, 따라서 락파일에 직접 손을 대거나 해서는 안된다. 대신 항상 루씬이 제공하는 락 기능을 그대로 사용하는 편이 좋다.

5. 색인작업 디버깅
루씬 색인에 문서를 추가하는 과정에서 디버깅해야 할 필요가 있다면 IndexWriterConfig의 setInfoStream 메소드로 루씬에게 색인과정에서 진행되는 내용을 SYstem.out 등의 PrintStream에 출력하게 지정할수있다.

package foo.bar;

import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queries.CustomScoreQuery;
import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.valuesource.LongFieldSource;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.RAMDirectory;

import java.io.IOException;

public class HelloApp {
    public static void main(String[] args) throws IOException, ParseException {
        RAMDirectory ramDirectory = new RAMDirectory();
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new WhitespaceAnalyzer())
                .setOpenMode(IndexWriterConfig.OpenMode.CREATE);
        IndexWriter indexWriter = new IndexWriter(ramDirectory, indexWriterConfig);
        indexWriterConfig.setInfoStream(System.out);
        Document doc = new Document();
        doc.add(new TextField("f", "test document", Field.Store.NO));
        doc.add(new NumericDocValuesField("boost", 1L));
        indexWriter.addDocument(doc);

        doc = new Document();
        doc.add(new TextField("f", "test document", Field.Store.NO));
        doc.add(new NumericDocValuesField("boost", 2L));
        indexWriter.addDocument(doc);
        indexWriter.close();

        IndexReader indexReader = DirectoryReader.open(ramDirectory);
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        Query baseQuery = new TermQuery(new Term("f", "test"));
        Query boostQuery = new FunctionQuery(new LongFieldSource("boost"));
        Query q = new CustomScoreQuery(baseQuery, (FunctionQuery) boostQuery);
        indexSearcher.search(q, 10);
    }
}

6. 삭제된 문서가 차지하는 디스크 공간
루씬은 삭제된 문서를 기록할 때 아주 간단한 방법을 사용한다. 즉 삭제한 문서는 삭제했다고 비트 배열에 삭제 여부만 표시하고 이런 표시 작업은 아주 빠르게 처리 할수 있지만 실제 문서는 아직 색인 안에 남아있는 상태이다.
일반적으로 역파일 색인에는 문서 하나를 텀으로 분리해 색인의 여러곳에 퍼져있는 형태 이기 때문에 삭제 할때마다 매번 해당 문서를 색인에서 삭제하려면 그다지 효율적이지 않다.
루씬은 문서를 추가하거나 삭제할 문서가 있다면 이런 변경사항은 디스크에 즉시 반영하는 대신 일단 메모리 버퍼안에 보관한다. 이렇게 메모리버퍼에 변경사하을 보관해두면 디스크 입출력 횟수를 줄여 성능을 개선 시킬수 있다. 그리고 주기적으로 Directory안에 새로운 세그먼트를 생성하고 메모리 버퍼의 변경사항을 디스크에 플러시한다.

메모리 버퍼에 지정된 용량 이상의 문서가 쌓이면 디스크에 플러시하며, 버퍼에서 최대로 사용할 수 있는 메모리 용량은 setRAMBufferSizeMB 메소드로 지정한다. 
setMaxBufferedDocs메소드로 메모리 버퍼에 보관할 문서의 최대 개수르 ㄹ지정하면 문서의 개수가 지정한 값에 다다를 때 세그먼트를 생성하고 메모리 버퍼의 내용을 플러시 한다. 
텀이나 질의로 삭제할때 setMaxBufferedDeleteTerms 메소드로 삭제한 텀이나 질의의 최대개수를 지정할 수 있다. 삭제된 텀이나 질의의 개수가 자정한 값에 도달하면 삭제된 내용을 디스크에 플러시 한다. 

위 세가지 상황 중 하나에 해당하면 메모리 버퍼의 내용을 플러시 한다. 필요한 경우 메모리 버퍼 사용량을 지정하는 메소드에 IndexWriter.DISABLE_AUTO_FLUSH인자를 지정하면 해당 상황이 발생해도 자동으로 플러시 하지 않게 제한 할수 있다. 그리고 루씬은 기본 설정으로 메모리 사용량이 16MB를 넘어가면 플러시 한다.

7. 색인 커밋
IndexWriter의 commit메소드를 호출하면 새로운 색인 커밋이 생성된다. IndexWriter의 commit메소드는 두개이며, commit()메소드는 색인 커밋을 새로 생성하고, commit메소드는 새로 생성하는 색인 커밋에 인자로 넘겨받은 추가 정보를 함꼐 보관한다. 일반적으로 커밋작업은 시스템 자원을 많이 소모하며, 따라서 커밋 작업을 너무 자주 호출하면 색인 성능이 떨어진다. 어떤 이유에서든지 혹시라도 직적 커밋이후 지금까지의 변경 사항을 모두 무시하려면 rollback()메소드를 호출하면 된다.

1. 메모리 버퍼에 들어있는 모든 새 문서와 삭제된 문서를 플러시 한다. 
2. IndexWriter를 새로 생성하거나 commit()메소드를 호출한 이후 메모리 버퍼를 플러시해 새로 생성한 파일과 병합작업을 통해 생성한 파일을 모두 싱크한다. 
3. 다음번호의 segments_N파일을 생성하고 디스크에 싱크한다. 이 작업이 끝나고 IndexReader를 다시 열면 변경된 내용을 볼 수 있다. 
4. IndexDeletionPolicy로 지정한 설정에 따라 기존의 커밋을 제거한다. 

8. 병합
색인에 새그먼트 개수가 너무 많아지면 IndexWriter에서 몇개의 세그먼트를 골라 하나의 세그먼트로 병합한다. 이렇게 세그먼트를 병합하면 몇가지 장점이 있다.

세그먼트의 개수가 줄어든다. 몇개의 세그먼트를 병합해 하나의 큰 세그먼트를 색인에 추가하고 병합한 몇개의 세그먼트는 모두 삭제한다. 세그먼트 개수가 줄어들면 검색 질의를 실행해야 할 횟수가 줄어들기 때무에 검색 성능이 빨라진다. 
세그먼트를 병합하면 색인에 차지하는 디스크 공간이 줄어든다. 예를 들어 삭제했다고 표시된 문서가 있다면 병합 과정에 실제로 디스크에서 제거된다. 

병합 작업에서 세그먼트의 기준을 정해주는 클래스가 MergePolicy이다. 정확히 말해 MergePolicy는 병합 작업에 해당하는 대상 세그먼트를 뽑아주고, 병합 작업을 실행하는 일은 MergeScheduler클래스가 맡고있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.