본문 바로가기
ES

엘라스틱서치의 루씬 검색 라이브러리

by 흰색남자 2022. 11. 22.

https://wedul.site/677

 

용어 정리

재현율

  • 검색 시스템에서 관련된 문서를 얼마나 빼먹지 않고 찾아두는지

 

정확도

  • 검색 시스템에서 사용자가 입력한 검색어와 관련없는 문서를 얼마나 정확하세 제거 하는지

 

fuzzy

  • 레빈슈타인 편집거리를 통해서 입력한 텀과 유사한 텀을 가진 문서를 찾아줌
  • 비교되는 두 단어의 추가, 수정, 삭제에 대한 비용 처리를 하며 비용이 높을수로 서로 다른 term

 

검색 모델

  • 순수 boolean 모델
    • 지정된 질의에 문서가 해당하는지 아니면 해당하지 않는지를 판단하며 별도의 계산 부분이 없다.
  • 벡터 공간 모델
    • 질의와 문서 모두 고차원(차원은 term을 의미)의 벡터로 표현.
    • 벡터간의 거리를 계산하면 문서와 질의 사이의 연관도나 유사도를 산출 할 수 있다.
  • 확률모델
    • 확률적인 방법을 통해 개별 문서가 질의와 일치하는 확률을 계산한다.

 

IndexWriter

  • 색인을 새로 생성하거나 기존 색인을 열고 문서를 추가하거나 삭제거하거나 변경하는 기능을 담당
  • Directory, Analyzer 옵션을 받아서 생성됨

 

Directory

  • 루씬의 색인을 저장하는 곳
  • 추상 클래스로써 구현하는 클래스에 따라서 위치를 자유롭게 바꿀 수 있다.
  • IndexWriter 생성인자로 사용됨

 

Analyzer

  • 본문이나 제목, 단어등을 텍스트를 색인하기 전에 반드시 analyzer를 거쳐서 단어로 분리를 해야한다.
  • IndexWriter 생성인자로 사용됨
  • stop word를 제거하거나 대, 소문자를 분리하는등의 여러 단어 분리에 작용을 함

 

Document

  • 개별 필드의 집합
  • Document는 하나이상의 field를 담고 있는 것이며 실제 생인 대상 텍스트는 모두 field에 저장됨

 

Field

  • Document 내부에 저장되는 field
  • Document 내부에는 같은 이름을 가진 필드가 2개 이상 들어갈 수 있으며 이런 경우 같은 이름 필드는 Document 객체에 추가된 순서대로 값을 연결해 색인한다. 즉 마치 두 필드의 값을 하나의 필드를 지정한 것처럼 동작한다.

 

IndexSearcher

  • IndexSearcher는 검색을 담당하는 클래스로 색인을 읽기 전용으로 열어서 사용

 

Term

  • 검색 과정을 구성하는 가장 기본적인 단위
  • term query는 인덱싱 시 analyzer를 통해 분리되어 색인되고 검색 시에는 임의에 term을 만들어서 해당하는 document를 찾을 때 사용할 수 있다.

 

Query

  • Query는 최상의 질의 클래스이고 필요에 따라 PhraseQuery, BooleanQuery, TermQuery등으로 만들어져서 사용한다.

 

루씬의 특징 정리

  • 텍스트를 토큰으로 변환해 역파일 색인 구조에 저장
  • 역파일 색인은 데이터를 빠르게 찾을 수 있으며 공간도 효율적
  • 역파일 색인은 문서 단위로 사용하지 않고 정렬된 토큰을 기준으로 조회
  • 루씬의 색인은 하나 이상의 세그먼트로 구성됨. 문서의 일부분이며 IndexWriter에서 추가하거나 삭제한 문서를 버퍼에 쌓아두고 있다가 flush되면 세그먼트를 만든다.
  • 조회시 모든 세그먼트를 조회한 후 결과를 합친다
  • 세그먼트 마다 역할이 있으며 그 역할에 따라 확장자가 다른데 통합 색인을 할경우 세그먼트에 모든 파일이 하나의 cfs파일에 들어감. 통합 색인은 성능의 영향이 있을 수 있으나 파일 오픈에 대한 이슈를 줄이는데 이점이 있음
  • 세그먼트가 많아지면 indexwriter가 mergescheduler클래스에 정의된 시점에 따라 세그먼트를 병합하고 기존 세그먼트를 삭제함
  • 삭제된 Document가 차지하던 공간은 바로 삭제하는 것이 아니라 삭제 표시를 진행하고 나중에 제거
  • maxDoc은 삭제가 되었어도 색인이 되어있는 문서수를 보여준다. 하지만 optimize를 통해 실제 공간이 정리되면 삭제된 문서수는 빼고 보여준다.
  • 루씬은 수정기능이 없고 수정된 문서를 삭제하고 재생성
  • 역파일 색인을 생성할 때 기본설정으로 벡터 공간 모델의 검색을 실행할 때 필요한 값을 모두 저장함. 벡터 공간 모델은 해당 문서에 특정 텀이 몇번이 나타나는지 횟수와 나타나는 위치를 저장한다. 하지만 단순 boolean 값의 경우 공간이나 횟수 등을 저장할 필요가 없기 때문에 Field.setOmitTermFreqAndPositions(true)를 설정해서 불필요한 디스크 공간 사용을 막을 수 있다.
  • 색인 시 Index.Analyzer와 Store.No를 같이 사용하여 텍스트 길이가 길어 원문은 필요없고 분석만 사용해서 나중에 검색에는 걸리도록 할 수 있다.
  • CompressionTools를 사용해서 데이터를 저장할 때 압축해서 저장가능하다.
  • term vector는 문서내에 유일한 텀의 개수 위치, 오프셋 등을 저장하고 있어 검색 시에 활용 된다.
  • 루씬에서는 같은 이름을 가진 필드를 여러개 가질 수 있고 들어오는 값의 순서에 따라 역파일 색인, 텀 벡터를 구성한다.
  • 루씬은 역색인 파일로써 문서 하나를 텀으로 분리해서 색인의 여러곳에 퍼져있기 때문에 문서를 삭제할 때 마다 매번 해당 문서를 색인에서 삭제하려면 비효율적이기 때문에 비트배열에 삭제 표시만 하고 시간이 지나면 자동으로 디스크 병합과정을 통해 디스크에서 삭제된다. (임의로 optimize를 할 수도 있으나 성능문제 있음)
  • IndexWirter가 commit, close를 하지 않으면 IndexReader는 변경사항을 볼 수 없다.
  • 데이터가 크면 여러 shard로 분리하고 결과를 각 shard에서 데이터를 조회하고 모아야하는데 루씬 자체에서는 확정성을 지원하는 분산처리 기능이 지원하지 않아서 별도의 solr elasticsearch 등을 사용해야 한다.
  • 루씬은 검색엔진이 아니라 검색 라이브러리이다.

 

 

norm

  • 루씬은 필드에 보관하고 있는 텍스트의 길이가 짧을수록 자연적으로 중요도가 높아진다. (루씬 내부에서 토큰 개수에 따라 중요도를 계산하는데 이 토큰의 개수가 적을수록 중요도가 높아진다.)
  • 루씬은 검색질의와 문서와의 유사도를 연관도 순위를 통해 순위를 결정하며 이때 각 필드 또는 문서의 중요도(boost)도 연관도 순위에 영향을 주는 요소중 하나이다.
  • 루씬은 중요도(boost)값을 norm에 저장한다.
  • norm을 직접 조작할 수 있다.
  • 각 필드와 문서 만큼 norm을 보유하고 있으면 메모리 사용량이 많이 늘어나기 때문에 메모리 절약을 위해 norm이 필요 없는 경우 Field.Index.NO_NORMS를 설정해주면 된다.

 

색인

색인 최적화

  • 색인 시 세그먼트가 생성되고 검색 시 각 세그먼트를 열고 검색하고 세그먼트의 결과를 취합한다. 수많은 세그먼트로 구성된 색인의 경우 최적화를 통해 세그먼트 수를 줄여 검색속도를 향상 시킬 수 있다. (단 최적화를 하지 않는다고 그렇게 검색속도가 느린건 아니기 때문에 꼭 필요한게 아니면 하지 않도록 한다.)
  • 색인 최적화 시 cpu자원과 디스크 입출력 대역폭을 많이 소모하기 때문에 꼭 필요하다고 판단되는 것이 아니면 최소한으로 사용하는 것이 좋다.
  • 최적화 작업 도중을 하려면 기존 세그먼트는 놔두고 추가로 생성되는 세그먼트를 만들기 때문에 새로운 세그먼트가 만들어지기 전까지는 기존 세그먼트도 같이 존재하게되어 디스크 용량을 많이 차지하게 된다. 물론 최적화가 끝나면 기존 세그먼트는 삭제된다. (대체로 3배의 디스크 용량이 필요하다.

 

병렬 처리, 스레드 안정성, 락

  • 루씬에서는 IndexReader를 여러개 만들 수 있으나 하나 생성 후 공용으로 사용하는 것이 좋다.
  • IndexWriter로 색인 작업 시 쓰기 락이 걸리기 때문에 작업을 동시에 진행할 수는 없다.
  • indexing 락은 write.lock 파일에 여부를 기록하고 있으며 락이 걸려 있는 도중 색인을 하려할 경우 LockObtainFailedException이 발생한다.
  • lock 파일을 임의로 조작해서는 안된다.
  • IndexReader는 IndexWriter 동작으로 락이 걸려있어도 언제든지 세그먼트를 열어 볼 수 있다.
  • IndexReader, IndexWriter는 여러 쓰레드에서 공용으로 사용할 수 있도록 잘 설계되어 있으므로 하나의 자원만을 활용하여 여러 쓰레드에서 사용하는게 좋다.

lock 테스트 코드 예제

IndexWriter로 indexing을 하고 있는 dir을 다른 IndexWriter가 접근하려할 시 LockObtainFailedException이 발생

package indexing;

import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import util.TestUtil;

import java.io.File;
import java.io.IOException;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

public class IndexingLock {

    private static Directory dir;
    private static File indexDir;

    public static void main(String args[]) throws IOException {
        setUp();
        testWriteLock();
    }

    protected static void setUp() throws IOException {
        indexDir = new File(System.getProperty("java.io.tmpdir", "tmp") + System.getProperty("file.separator") + "index");
        dir = FSDirectory.open(indexDir);
    }

    // writer1이 indexing dir 사용하고 있어서 writer2가 접근하려 하면 색인 락에 의해서 LockObtainFailedException이 발생
    public static void testWriteL을ock() throws IOException {
        IndexWriter writer1 = new IndexWriter(dir, new SimpleAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
        IndexWriter writer2 = null;

        try {
            writer2 = new IndexWriter(dir, new SimpleAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);
            fail("we should never reach this point");
        } catch (LockObtainFailedException e) {
            e.printStackTrace();
        } finally {
            writer1.close();
            assertNull(writer2);
            TestUtil.rmDir(indexDir);
        }
    }

}

 

 

문서 버퍼, 플러시

  • 루씬은 색인에 문서를 추가하거나 삭제할 때 변경사항을 메모리 버퍼에 넣어놓고 추후 한번에 반영하여 디스크 입출력 횟수를 줄여 성능을 개선 시킨다.
  • 실제 버퍼에서 디스크를 기록하려고 할 때 정해진 메모리 버퍼(기본 16mb)가 넘어설 때, 보관하고자 하는 문서 개수가 넘어설 때, 텀이나 질의로 삭제를 할 때 삭제한 텀이나 질의에 개수가 최대 개수를 넘어설 때 flush를 진행한다.

 

세그먼트 병합 (merge)

  • 세그먼트가 많아지면 IndexWriter에서 몇 개의 세그먼트를 골라 하나의 세그먼트로 병합한다.
  • 병합을 통해 세그먼트가 줄어들면 열어야할 세그먼트가 줄어들어 검색속도가 빨라지고 디스크 공간도 작아진다.
  • 세그먼트를 진행하는 기준인 세그먼트가 많다는 것은 MergePolicy 클래스에 따라 정해진다. MergeScheduler에서 스케쥴을 돌면서 MergePolicy를 통해 추출되는 대상 세그먼트를 병합한다.

 

루씬 관리와 성능 튜닝

  • disk는 가능하다면 ssd를 사용하자.
  • 루씬 버전을 교체한다. 루씬의 경우 버전이 올라갈수록 성능 최적화가 이루어 지기 때문에 자주 올리는게 좋다.
  • JVM 버전을 업그레이드 하자.
  • 네트워크 디스크 장비가 아닌 로컬 디스크 장비를 사용하자.
  • 자바 프로파일러를 사용하여 트래킹 하자
  • IndexReader, IndexWriter, IndexSearcher 등은 인스턴스를 한번 생성하고 나면 최대한 여러 곳에서 사용하는 것이 좋다.
  • 병렬 처리를 사용하자
  • cpu/ io 성능이 좋은 장비를 사용하자.
  • 사용하지 않은 필드나 기능은 과감하게 제거하자
  • 단독으로 사용하지 않는 여러 필드의 내용은 하나의 필드로 묶는 것이 좋다.
  • 정렬 가능성이 있는 필드 질의를 먼저 사용해서 필드 캐시를 먼저 채우게 한다.
  • 디스크에서 데이터 값을 계속 가지고 오면 CPU, I/O 자원을 많이 소모하기 때문에 많이 사용하는 데이터는 필드 캐시에 올려서 사용하자
  • 메모리에가 충분 하다면 메모리 버퍼를 크게 사용하자. 하지만 메모리 버퍼를 너무 많이 사용하면 JVM에서 가비지 컬렉션 작업을 자주 사용하고 운영체제 수준에서 메모리 페이지 작업을 자주 발생 시키기 때문에 성능 테스트를 잘 확인해야한다.
  • 검색 시 필요한 필드만 조회해서 사용한다.
  • optimize를 사용하여 파편화된 세그먼트를 재 조립한다.
  • 멀티 쓰레드를 색인과 검색의 성능을 높일 수 있다. 하지만 무작정 늘린다고 좋아지는 건 아니다. 적절한 수준을 넘어서게 되면 더 이상 처리량이 증가하지 않고 심지어 줄어들수도 있다. (쓰레드가 늘어나면서 대기 시간 증가)

 

 

 

출처 : 루씬인 액션 (고성능 오픈소스 자바 검색 엔진) - 에이콘

'ES' 카테고리의 다른 글

엘라스틱 서치가 검색에 빠른 이유  (3) 2022.12.17
elasticsearch  (0) 2022.10.27