## 목차 ##



## 1. 루씬 데이터 모델 ##

(1) 문서와 필드

  • 색인, 검색 작업에서 '한 건'이라고 부를 수 있는 단위

    • 하나 이상의 필드를 포함하며 필드에 실제 내용이 저장된다.
  • 색인 시의 필드 처리 작업

    • 필드 내용을 색인할 것인지 하지 않을 것인지 여부
      • 텍스트로 된 필드만 색인 가능
      • 색인한다면 텍스트 분석기로 토큰을 추출하고 토큰 단위로 색인한다.
    • 필드 내용을 저장할 것인지 하지 않을 것인지 여부
      • 분석 절차를 거치지 않은 텍스트가 색인에 그대로 저장
      • 보통 제목과 같이 사용자에게 그대로 보여줄 필요가 있는 필드에 사용
      • 저장을 하지 않은 필드는 결과 Document에 포함되지 않는다.



(2) 유연한 스키마

  • 별도의 스키마가 존재하지 않는다.

  • 각 문서로 서로 다른 필드 구조를 가져도 된다.

    • 필드 수, 이름 등이 모두 달라도 된다.
  • 색인에 서로 다른 종류의 문서를 넣어둘 수도 있다.



(3) 비정규화

  • 색인할 때는 항상 루씬이 지원하는 문서 표현 방식으로 원본 파일을 변형해야한다.
    • XML, JSON 등



결국 색인을 하게되면 색인된 필드 내용에 포함된 토큰을 기준으로 문서를 검색할 수 있게 된다.

색인을 통해 우리는 결과물(index) Map<Term, Document>를 얻게된다.



## 2. 색인 절차 ##

lucene_indexing_process



(1) 텍스트 추출과 문서 생성

  • 색인을 위해 문서의 제목, 내용 등의 텍스트를 추출해야한다.

  • 색인을 위한 루씬 문서 객체를 만든다.

Document doc = new Document();
doc.add(new Field("contents", new FileReader(file))); //색인o, 저장x
doc.add(new Field("filename", file.getName(), Field.Store.YES, Field.Index.NOT_ANALYZED)); //색인x, 저장o
doc.add(new Field("fullPath", file.getCanonicalPath(), Field.Store.YES, Field.Index.NOT_ANALYZED)); //색인x, 저장o

특정 단어를 포함하는 문서를 검색하기 위한 구조의 Document 생성하기 위해 "contents" 필드만 색인을 지정하고 나머지는 그대로 내용을 저장했다.



(2) 분석

  • IndexWriter가 Docuemnt 객체를 넘겨받으면 분석을 진행한다.

  • 분석을 보조해주는 매우 많은 필터가 있다.

    • ex) 불용어 제거 필터, 기본형 변환 필터, 대소문자 변환 필터 등
  • 색인을 하도록 지정한 필드는 토큰 스트림으로 변환된다.



(3) 색인에 토큰 추가

  • 토큰을 역파일 색인(inverted index) 구조에 추가한다.

    • 역파일 색인은 정렬된 토큰을 기준으로 검색한다.
  • 색인 세그먼트(index segment)

index_segments

  • 세그먼트는 색인의 일부분이다.

  • 새로 추가하는 Document는 새로 생성한 세그먼트에 추가하고 필요할 때마다 여러 세그먼트를 병합하여 적절한 세그먼트 수 유지

    • 이러한 절차를 통해 색인 변경 작업이 최소화된다.
  • 세그먼트 덕분에 증분 색인(incremental indexing)이 가능해지고 문서 추가로 인한 전체 재색인이 필요없다.



## 3. 기본 색인 작업 ##

(1) 색인에 문서 추가

  • addDocument(Document)

    • IndexWriter객체 생성 시 지정한 분석기를 사용해 문서를 색인에 추가
  • addDocument(Document, Analyzer)

    • 매개변수로 지정한 분석기를 사용해 문서를 색인에 추가
Directory indexDir = FSDirectory.open("~/workspace/indexDest");
Directory dataDir = FSDirectory.open("~/workspace/dataSource");

IndexWriter writer = new IndexWriter(dir, new WhiteSpaceAnalyzer(), IndexWriter.MaxFieldLength.UNLIMITED);

File[] files = new File(dataDir).listFiles();
for(File file : files) {
    Document doc = new Document();

    //본문은 저장하지않고 분석하여 색인
    doc.add(new Field("contents", new FileReader(file),
                        Field.Store.NO,
                        Field.Index.ANALYZED));

    //제목은 저장하고 분석 없이 색인
    doc.add(new Field("title", file.getName(),
                        Field.Store.YES,
                        Field.Index.NOT_ANALYZED));

    //경로는 저장하고 색인하지 않음
    doc.add(new Field("path", file.getCanonicalPath(),
                        Field.Store.YES,
                        Field.Index.NO));

    //색인에 문서 추가
    writer.addDocument(doc);
}

본문같은 경우는 저장하지 않아 문서를 검색했을 때 문서에 포함되지 않는다. 색인시 분석하도록 설정하여 토큰스트림 생성 후 토큰을 기준으로 색인이 될 것이다.

제목은 저장을 했기 때문에 문서에 포함된다. 그러나 분석없이 색인하여 제목 전체를 정확하게 입력해주어야 검색이 될 것이다.

경로는 저장을 했기 때문에 문서에 포함된다. 그러나 색인은 하지 않아 경로를 통한 검색은 할 수 없다.

addDocuement() 호출을 통해 색인에 문서를 추가한다. doc 객체에 필드를 추가할 때 설정한 대로 저장 및 분석, 색인을 진행한다.



(2) 색인 문서 삭제

  • IndexWriter 클래스는 다양한 삭제 메소드를 제공

    • deleteDocuments(Term)
      • 지정한 Term을 포함하는 모든 문서 삭제
    • deleteDocuments(Term[])
      • Term 배열의 Term 중 하나라도 포함하는 모든 문서 삭제
    • deleteDocuments(Query)
      • Query에 해당하는 모든 문서 삭제
    • deleteDocuments(Query[])
      • Query[] 배열에 담긴 Query 중 하나라도 해당하는 문서 삭제
    • deleteAll()
      • IndexWriter가 가르키는 색인의 모든 문서 삭제
  • 문서마다 고유한 식별자(ID) 필드가 존재하면 해당 필드 값으로 특정 문서를 정확히 삭제할 수 있다.

    • ex) `writer.deleteDocuments(new Term("id", 8));
  • 삭제 연산 후 IndexWriterclose() 혹은 commit() 호출하면 색인에 반영


public static void delete(String indexDir) throws IOException {
   Directory dir = FSDirectory.open(new File(indexDir));

   IndexWriter writer = new IndexWriter(dir, new WhitespaceAnalyzer(),
           IndexWriter.MaxFieldLength.UNLIMITED);

   Term term1 = new Term("contents", "patent"); //contents에 "patent"를 포함하는 모든 문서 삭제
   Term term2 = new Term("contents", "disadvantages"); //contents에 "disadvantages"를 포함하는 모든 문서 삭제

   Term[] deleteTerms = {term1, term2};

   for(Term term : deleteTerms) {
       writer.deleteDocuments(term);
   }

   writer.close(); //삭제가 색인에 반영
}

삭제한 문서는 색인에서 완전히 제거되지 않고 삭제 표시가 된다.

writer.maxDocs()는 제거된 문서까지 포함한 문서 수를 반환하고 writer.numDocs()는 제거된 문서를 제외한 문서 수를 반환한다.

writer.optimize()는 강제로 루씬이 최적화 작업을 실행하도록 한다. 그러면 삭제 표시된 문서가 완전히 색인에서 제거된다.



(3) 색인 문서 변경

  • IndexWriter의 문서 변경 메소드 호출

    • `updateDocument(Term, Document);
    • `updateDocument(Term, Document, Analyzer);
  • updateDocument()는 내부적으로 deleteDocuments()를 호출한 후 addDocuemnt()를 호출할 뿐이다.

    • updateDocuemnt(Term, Docuemnt)는 Term이 포함된 문서를 삭제하고 전달받은 Document를 새로 색인에 추가한다.



## 4. 필드별 설정 ##

  • Field 객체를 생성할 때는 색인할 텍스트 이외에도 추가적인 처리 작업을 지정해준다.



(1) 색인 관련 설정

  • Field.Index.*

  • ANALYZED

    • 필드 내용을 분석기에 넘겨 토큰 스트림을 만들어 내고 토큰을 기준으로 검색이 가능하게 한다.
    • ex) 본문, 제목, 요약
  • NOT_ANALYZED

    • 필드 내용을 검색 가능하도록 역파일 색인에 추가하긴 하지만 분석기로 처리하진 않고 통째로 넣는다.
    • URL, 절대경로, 사람 이름, 주민등록번호, 전화번호
  • ANALYZED_NO_NORMS

    • ANALYZED와 비슷하지만 norm 값을 지정하지 않는다.(norm 값은 이후 챕터에 설명)
  • NOT_ANALYZED_NO_NORMS

    • NOT_ANALYZED와 비슷하지만 norm값을 지정하지 않는다.
  • NO

    • 필드의 내용이 색인되지 않는다. 즉, 검색할 수 없다.


  • 벡터 공간 모델에서 사용할 용어 빈도수와 위치를 색인하지 않게 설정
    • 디스크 공간 절약, 검색과 필터 적용 시간 단축
Field field = new Field("path", file.getCanonicalPath());
field.setOmitTermFreqAndPositions(true);
doc.add(field);

절대 경로는 연관도 점수에 영향을 줄 필요가 없으므로 용어 빈도수와 위치를 색인하지 않도록 설정한 코드이다.



(2) 저장 관련 설정

  • Field.Store.*

  • YES

    • 필드 내용을 색인에 그대로 저장
    • 검색결과 문서에 색인 내용을 받아볼 수 있다.
    • ex) URL, 제목, 절대경로
  • NO

    • 필드 내용을 색인에 저장하지 않는다.
    • 주로 Field.Index.ANALYZED와 같이 사용
    • 필드 내용의 토큰으로 검색해야하지만 내용이 길어 저장은 할 필요가 없는 경우에 사용
    • ex) 문서의 본문



(3) 텀 벡터 관련 설정

  • Term Vector란?

    • 벡터 공간 모델 검색에 필요한 정보(단어가 출현하는 문서 내 위치, 출현 빈도)
    • Map 형태의 자료
    • 필드마다 텀 벡터를 가진다.
    • 검색결과로 확보한 Document 내부에서 정보를 찾는 문서 내의 역색인으로 보면된다.
  • Field 인스턴스 생성시 텀 벡터 설정을 위한 상수를 전달한다.

  • TermVector.YES

    • 필드의 모든 텀과 출현 빈도를 저장
  • TermVector.WITH_POSITIONS

    • 텀의 위치를 같이 저장
  • TermVector.WITH_OFFSETS

    • 텀의 오프셋(단어의 시작 위치와 종료 위치)를 같이 저장
  • TermVector.WITH_POSITIONS_OFFSETS

    • 텀의 위치와 오프셋을 같이 저장
  • TermVector.NO

    • 텀 벡터 정보를 저장하지 않는다.



(4) Reader, TokenStream, byte[] 필드

  • Field는 String 자료형이 아닌 내용을 가질 수 있다.

    • 이를 위한 여러가지 생성 메소드가 오버로딩 되어있다.
  • Field(String name, Reader value, TermVector termVector)

    • String 대신 Reader 객체를 넣어줄 수 있다.
    • 이 경우에는 필드의 내용을 색인에 저장할 수 없고 항상 Field.Store.NO로 고정된다.
  • Field(String name, Reader value)

    • TermVector.NO로 고정
  • Field(String name, TokenStream tokenStream, TermVector termVector)

    • 분석이 끝난 TokenStream을 넣어줄 수 있다.
    • 이 경우에는 필드의 내용을 색인에 저장할 수 없고 항상 Field.Store.NO로 고정된다.
  • Field(String name, TokenStream tokenStream)

    • TermVector.NO로 고정
  • Field(String name, byte[] value, Store store)

    • 이진 데이터를 저장할 때 사용한다.
    • 색인할 수 없어 Field.Index.NO로 고정된다.
    • TermVector.NO로 고정된다.
  • Field(String name, byte[] value, int offset, int length, Store store)

    • offset과 length로 지정된 byte배열의 일부만 사용한다.



(5) muti-valued 필드

  • 같은 이름을 가진 필드가 여러개 있어도 된다.
Document doc = new Document();
for(String author : authors) {
    doc.add(new Field("author", author, 
                        Field.Store.YES,
                        Field.Index.NOT_ANALYZED));
}



## 5. 문서와 필드 중요도 ##

  • 필드와 문서별로 중요도를 지정하여 색인이 가능하다.
    • 검색 과정에서 중요도 지정을 할 수도 있지만 CPU 자원이 좀 더 많이 사용된다.



(1) 문서 중요도

  • 문서의 중요도 지정
    • 기본값은 1.0f이다.
doc1.setBoost(1.5f); //상대적으로 높은 중요도
doc2.setBoost(0.5f); //상대적으로 낮은 중요도



(2) 필드 중요도

  • 필드마다 별도로 중요도를 지정할 수 있다.

    • 기본값은 1.0f이다.
  • 문서 중요도 지정은 사실 문서의 모든 필드의 중요도를 지정한 것이다.

field.setBoost(1.2f); //상대적으로 높은 중요도
field.setBoost(0.3f); //상대적으로 낮은 중요도



(3) norm

  • 필드마다 중요도를 norm값으로 저장한다.

    • 사용자가 설정한 값과 토큰 길이에 따른 중요도 값을 모두 합해 단일 바이트 값으로 변환하여 저장한다.
  • IndexReader 클래스의 setNorm() 메소드로 변경할 수 있다.

    • ex) 최신 문서 혹은 클릭수가 높은 문서에 중요도 부여



## 6. 숫자, 날짜, 시각 색인 ##

(1) 숫자 색인

  • NumericField 클래스 사용

    • set<Type>Value() 메소드로 값 지정
    • int, long, float, double 등의 숫자를 직접 설정
  • 각 숫자 값은 트라이(trie) 구조로 색인

    • 질의에 해당하는 트라이 공간을 찾아낸 다음 공간의 모든 문서를 가져올 수 있다.
    • 범위 검색이나 범위 필터를 매우 빠르게 처리
  • Document에 이름이 같은 NumericField를 여러개 추가할 수 있다.

    • 필드 기준으로 정렬이 필요한 경우는 같은 이름을 사용하지 않아야한다.



(2) 날짜, 시간 색인

  • 시간
    • Date.getTime()으로 long값으로 변환
doc.add(new NumericField("timestamp").setLongValue(new Date.getTime()));
doc.add(new NumericField("timestamp").setLongValue(new Date.getTime()/3600/24);
  • 날짜
    • Calendarget() 메소드 사용
doc.add(new NumericField("dayOfMonth").setIntValue(Calendar.getInstance().get(Calendar.DAY_OF_MONTH)));



## 7. 필드 길이 ##

  • 필드의 최대 길이를 설정할 수 있다.

    • IndexWritersetMaxFieldLength()
    • IndexWriter의 생성자에 매개변수로 최대 길이를 나타내는 상수 전달
  • 불가피한 경우가 아니라면 최대 길이를 사용하지 않는게 좋다.

    • 길이를 넘어서는 뒷부분의 내용은 완전히 무시하게 되고 오류로 인식할 수 있다.



## 8. 색인 최적화 ##

  • 최적화를 통해 검색 속도 향상

    • 대량 문서 색인이나 여러 IndexWriter로 색인하는 경우 세그먼트 수가 많아진다.
    • 운영체제의 파일 개방 개수도 절약할 수 있다.
  • optimize()

    • 색인의 세그먼트를 하나로 병합
    • 병합 작업동안 대기
  • optimize(int maxNumSegments)

    • 부분 최적화
    • 세그먼트 수를 maxNumSegments 이하로 병합한다.
  • optimize(boolean doWait)

    • doWaitfalse로 지정하면 병합 작업을 백그라운드 스레드에서 진행
    • ConcurrentMergeScheduler와 같이 병합 작업을 백그라운드에서 실행하는 스케줄러를 사용하고 있어야 한다.
  • optimize(int maxNumSegments, boolean doWait)

    • 병합 작업을 백그라운드에서 실행하면서 최대 세그먼트 개수도 지정
  • 최적화 작업이 없다해도 검색 속도는 그다지 느려지지 않으니 꼭 필요한지 판단해야한다.

    • 최적화 작업에는 CPU자원과 디스크 입출력 자원이 많이 소모된다.



## 9. 여러 종류의 Directory ##

  • Directory 클래스는 간단한 파일 형식의 저장 공간을 나타내는 API이다.
    • Directory를 상속받은 하위 클래스 내부에 색인을 저장하기 위한 구현이 있다.
    • 색인 읽기/쓰기는 항상 Directory의 메소드를 통해 처리한다.


  • SimpleFSDirectory
    • 임의 위치에서 내용을 읽어오는 기능을 지원하지 않기 때문에 내부적으로 lock을 사용하여 동시 읽기 작업의 경우 성능이 떨어진다.


  • NIOFSDirectory
    • java.nio 패키지에서 제공하는 파일 내부 위치지정 기능을 사용하기때문에 lock을 사용하지 않는다.
    • MS Windows 운영체제의 경우 JRE 버그 때문에 성능이 떨어진다.


  • MMapDirectory
    • Memory Mapped I/O를 사용
    • lock을 사용하지 않는다.
    • Java는 메모리 맵 상태의 파일을 깔끔하게 해제하는 기능이 없어 GC가 동작해야 실제 메모리가 해제된다.


  • FSDirectory.open() 정적 메소드를 사용하면 좋다.
    • 운영체제 등의 환경에 따라 가장 적절한 파일시스템 기반 Directory를 만들어준다.


  • RAMDirectory
    • 색인을 메모리에 저장
    • 색인 크기가 작은 경우에 유용
    • 최신 OS는 여유 메모리를 활용해 디스크 I/O를 빠르게 처리해주므로 엄청난 수준의 속도 향상은 일어나지 않는다.



## 10. 병렬 처리, 스레드 안정석, 락 ##

(1) 스레드와 다중 JVM 안정성

  • 색인의 IndexReader는 몇 개라도 열어 사용할 수 있다.

    • 색인과 같은 JVM에 있거나 다른 JVM에 있거나 상관없다.
    • 색인과 같은 장비에 있거나 다른 장비에 있거나 상관없다.
    • 그러나 성능, 자원활용 측면에서 하나의 IndexReader 인스턴스를 생성하고 공유해 사용하는것이 낫다.
  • 색인 하나당 IndexWriter는 하나만 열어 사용할 수 있다.

    • 루씬에서는 쓰기 lock을 사용한다.
    • IndexWriter 객체가 생성되자마자 쓰기 lock을 확보한다.
  • IndexReaderIndexWriter가 열려 있는 도중에도 사용할 수 있다.

    • IndexReader가 생성되는 시점의 색인을 사용한다.
    • IndexWriter가 변경사항을 반영하더라도 IndexReader를 새로 열기 전에는 반영사항이 보이지 않는다.
    • IndexWriter 생성 메소드에서 create = true 인자를 사용해도 IndexReader 오픈 시점의 색인은 그대로 사용할 수 있다.
  • IndexReaderIndexWriter는 thread-safe 하므로 여러 스레드가 공유 가능하다.



(2) 색인 lock

  • 색인 하나에 하나의 IndexWriter 혹은 변경작업을 하는 IndexReader만 접근하도록 파일 기반의 lock 사용

    • 색인 디렉토리에 write.lock 혹은 reader.lock 파일을 통한 lock
    • lock 파일이 존재할 때 다른 자원이 lock을 획득하려 시도하면 LockObtainFailedException 발생
  • 루씬의 lock이 동작하는 형태를 제어할 수 있는 API 제공

    • LockFactory 클래스 상속 및 구현
    • Directory.setLockFactory() 메소드로 LockFactory 인스턴스를 지정하면 된다.
    • setLockFactory() 메소드는 IndexWriter 객체 생성 전에 호출해야한다.
    • NativeFSLockFactory, SimpleFSLockFactory, SingleInstanceLockFactory, NotLockFactory
  • IndexWriter.isLocked(Directory)

    • 색인에 쓰기 lock이 걸려있는지 확인
  • IndexWriter.unlock(Directory)

    • 색인의 lock을 강제로 해제
  • 루씬에서 기본적으로 제공하는 NativeFSLockFactory를 그대로 사용하는 편이 좋다.



## 11. 색인 작업 디버깅 ##

  • IndexWriter.setInfoStream(Stream)
    • 색인 작업 내용이 지정된 스트림으로 출력



## 12. 고급 색인 기법 ##

'프레임워크 > 루씬(Lucene)' 카테고리의 다른 글

(루씬) 4 - 고급 검색 기법  (0) 2020.05.16
(루씬) 3 - 검색(Search)  (0) 2020.05.16
(루씬) 1 - 루씬과의 만남  (0) 2020.05.16

+ Recent posts