## 목차 ##



## 1. 루씬이란? ##

  • 검색 기능을 제공하는 라이브러리(IR 라이브러리)

    • 검색 애플리케이션이 아닌 색인과 검색 기능을 제공하는 도구이다.
    • 아파치 재단의 오픈소스 프로젝트
    • Java를 기본으로 C++, C, Perl 등으로 포팅 되어있다.
  • 간결하고 강력하다.

    • JAR파일 크기 1MB
  • 색인과 검색 기능만 제공

    • 문서 수집, 텍스트 추출, 관리 등은 프로그래머가 직접 처리해야한다.



## 2. 루씬과 검색 애플리케이션의 구조 ##

  • 검색 애플리케이션의 구조
    • 원문 분석 및 처리하여 색인 형식에 맞는 문서(루씬 문서) 생성
    • 문서 텍스트 분석
    • 색인에 문서 추가
    • 사용자가 질의 생성
    • 질의 실행하여 색인에서 문서 검색
    • 결과 출력

search_application_components



(1) 색인 과정 구성 요소

문서를 통채로 두고 검색을 하기위해서는 일일이 모든 문서를 대상으로 단어나 구문을 찾아야한다. 문서의 양이 많아지고 한 문서의 크기가 커질수록 속도는 느려진다. 원문에서 단어를 추출하고 검색하기 좋은 형태의 문서로 만들어 리스트화 해두면 특정 단어의 위치로 바로 이동할 수 있다. 이러한 과정을 색인(indexing)이라하고 결과물도 색인(index)라고 한다.

  • 검색 대상 텍스트 확보

    • 크롤러(crawler)가 색인할 대상 문서 수집
  • 루씬 문서 생성

    • 원본문서를 루씬의 개별 문서 단위(document)로 변환
    • 텍스트를 추출하고 원하는 텍스트만 필터링하는 작업 필요
    • 루씬 문서는 여러 필드로 구성(제목, 본문, 저자, 링크 등)
  • 문서 텍스트 분석

    • 토큰(token) 생성
    • 어떤 단위로 토큰 할 것인지 결정 필요
    • 동의어, 단수/복수형, 기본형, 대소문자 등의 이슈 처리
    • 루씬 프로젝트에는 다수의 텍스트 분석기 내장
  • 색인에 문서 추가

    • 색인 과정이 끝난 문서를 색인에 추가



(2) 검색 과정 구성 요소

검색(search)은 색인에 있는 토큰을 기준으로 토큰이 포함된 문서를 찾아내는 과정이다.

  • 사용자 인터페이스

    • 사용자가 검색을 위해 사용하는 화면
    • 결과 표시 방법(글꼴, 정렬 등) 결정
    • 가장 중요한 부분
  • 검색 질의 생성

    • 전달받은 검색어를 통해 Query 객체 생성
    • 루씬의 QueryParser 클래스를 통해 처리
    • Query 객체 생성 과정에서 가중치나 필터를 적용할 수 있다.
  • 검색

    • 색인에서 Query 객체에 해당하는 문서 검색
    • 루씬은 '벡터 공간 모델'과 '불리언 모델' 사용
    • 검색 결과를 일련의 순서로 정렬
  • 결과 출력

    • 결과를 사용자 화면에 출력



## 3. 예제 애플리케이션 ##

(1) 색인 생성

아래 링크에서 sourcecode.zip 다운로드 후 lia2e/src/lia/meetlucene/data 에 있는 txt 파일 색인한다.

https://www.manning.com/books/lucene-in-action-second-edition

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;

import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;

public class Indexer {
   public static void main(String[] args) throws IOException {
       if(args.length != 2) {
           throw new IllegalArgumentException("Usage: java " +
                   Indexer.class.getName() + "<index dir><data dir>");
       }

       String indexDir = args[0]; // 색인 생성 디렉터리
       String dataDir = args[1]; // 색인 할 데이터 디렉터리

       long start = System.currentTimeMillis();

       Indexer indexer = new Indexer(indexDir);

       int numIndexed = 0;

       try {
           numIndexed = indexer.index(dataDir, new TextFilesFilter());
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           indexer.close();
       }

       long end = System.currentTimeMillis();

       System.out.println("Indexing " + numIndexed + " files took " + (end - start) + " milliseconds");
   }

   private IndexWriter writer;

   //Index 생성자
   public Indexer(String indexDir) throws IOException {
       Directory dir = FSDirectory.open(new File(indexDir));

       writer = new IndexWriter(dir,
               new StandardAnalyzer(Version.LUCENE_30),
               true,
               IndexWriter.MaxFieldLength.UNLIMITED);
   }

   public void close() throws IOException {
       writer.close();
   }

   public int index(String dataDir, FileFilter filter) throws Exception {
       File[] files = new File(dataDir).listFiles();

       for(File file : files) {
           if(!file.isDirectory() && !file.isHidden() && file.exists() && file.canRead() &&
                   (filter == null || filter.accept(file))) {
               indexFile(file);
           }
       }

       return writer.numDocs(); //색인된 문서 건수 리턴
   }

   private static class TextFilesFilter implements FileFilter {
       public boolean accept(File file) {
           return file.getName().toLowerCase().endsWith(".txt"); // txt 파일만 색인
       }
   }

   protected Document getDocument(File file) throws Exception {
       Document doc = new Document(); // 루씬 문서 생성
       doc.add(new Field("contents", new FileReader(file))); // 내용 필드 추가
       doc.add(new Field("filename", file.getName(), Field.Store.YES, Field.Index.NOT_ANALYZED)); // 파일 이름 필드 추가
       doc.add(new Field("fullpath", file.getCanonicalPath(), Field.Store.YES, Field.Index.NOT_ANALYZED)); // 절대 경로 추가

       return doc;
   }

   private void indexFile(File file) throws Exception {
       System.out.println("Indexing: " + file.getCanonicalPath());
       Document doc = getDocument(file);
       writer.addDocument(doc); // 루씬 문서 색인에 추가
   }
}

Indexer 클래스의 main()을 실행할 때 순서대로 아래의 두 파라미터를 전달해줘야한다.

+ 인덱스 된 문서를 저장할 디렉터리 경로명
+ 인덱싱 할 원본 문서 디렉터리 경로명

실행을 하면 색인이 완료되고 색인된 문서 수와 걸린 시간이 콘솔 출력으로 나타난다.

...

Indexing 16 files took 2587 milliseconds

Process finished with exit code 0



중요한 점은 색인에 문서가 반영되는 시점은 IndexWriterclose() 메소드가 호출될 때이다. 혹은 IndexWritercommit() 메소드를 실행하면 닫지 않고도 반영할 수 있다.



(2) 색인 내용 검색

Indexer 클래스가 색인한 문서를 검색해볼 차례다.

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;

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

public class Searcher {

   public static void main(String[] args) throws IllegalArgumentException, IOException, ParseException {
       if(args.length != 2) {
           throw new IllegalArgumentException("Usage: java" +
                   Searcher.class.getName() + " <index dir> <query>");
       }

       String indexDir = args[0]; //색인 디렉토리
       String q = args[1]; //질의어(검색어)
       search(indexDir, q);
   }

   public static void search(String indexDir, String q) throws IOException, ParseException {
       Directory dir = FSDirectory.open(new File(indexDir));
       IndexSearcher is = new IndexSearcher(dir); //색인 열기

       /* 질의 분석 -> "contents" 필드에서 검색 */
       QueryParser parser = new QueryParser(Version.LUCENE_30,
               "contents",
               new StandardAnalyzer(Version.LUCENE_30));
       Query query = parser.parse(q);

       long start = System.currentTimeMillis();
       TopDocs hits = is.search(query, 10); //색인에서 질의 검색
       long end = System.currentTimeMillis();

       System.err.println("Found " + hits.totalHits +
               " document(s) (in " + (end - start) + " milliseconds) that matched query " + q + ":");

       for(ScoreDoc scoreDoc : hits.scoreDocs) {
           Document doc = is.doc(scoreDoc.doc); //결과 문서 확보
           System.out.println(doc.get("fullPath")); //문서 경로 출력
       }

       is.close(); //IndexSearcher 객체 리소스 해제
   }
}

Searcher 클래스의 main()을 실행할 때 순서대로 아래의 두 파라미터를 전달해줘야한다.

+ 색인이 저장된 디렉터리 경로명
+ 검색 질의

위 코드에서 TopDocs 객체에는 실제 문서가 아닌 문서에 대한 메타 정보만 들어가있다. 실제 문서를 받아올 때는 IndexSearcher.doc(int)를 호출해 실제 Document 객체를 가져올 수 있다.



Indexer 클래스가 만들어둔 색인의 경로를 넣어주고 검색 질의로 "patent"를 주고 실행해보았다.

Found 8 document(s) (in 26 milliseconds) that matched query patent:

/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/cpl1.0.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/mozilla1.1.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/epl1.0.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/gpl3.0.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/apache2.0.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/lpgl2.0.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/gpl2.0.txt
/home/nobbaggu/workspace/lia2e/src/lia/meetlucene/data/lgpl2.1.txt

Process finished with exit code 0

출력된 경로들은 "patent"라는 문자열을 포함하는 문서들의 절대 경로다.



## 4. 색인 관련 핵심 클래스 ##

(1) IndexWriter

  • 색인 생성 및 기존 색인을 열고 Document 추가 및 삭제

  • 객체 생성자로 색인을 저장할 Directory 인스턴스를 넘겨주어야한다.

  • 객체 생성자로 분석기를 넘겨주어야 개별 단어를 구분하고 색인 작업을 할 수 있다.



(2) Directory

  • 루씬 색인을 저장할 공간을 나타낸다.

  • 추상 클래스



(3) Analyzer

  • 색인을 위한 단어 분리

  • 추상 클래스

  • 루씬에는 Analyzer를 상속받은 다양한 분석기 클래스가 있다.

    • 불용어 제거기, 대소문자 변환기 등



(4) Document

  • 검색 결과 단위가 되는 색인 문서

  • 여러 필드를 가지고 있다.

  • Document와 Field를 어떻게 구성할지 설계하는 작업이 필요하다.



(5) Field

  • 색인의 Document가 가지는 내용

  • 하나의 Document는 여러 필드를 가진다.

  • 같은 이름의 필드는 추가된 순서대로 연결하여 색인한다.



## 5. 검색 관련 핵심 클래스 ##

(1) IndexSearcher

  • 검색을 담당하는 클래스

  • 색인을 읽기 전용으로 열어 사용한다.

    • 생성자를 호출할 때 색인 디렉토리를 매개변수로 넘겨준다.



(2) Term

  • 검색 과정을 구성하는 가장 기본적 단위

  • (필드이름, 필드에 속한 특정 단어) 쌍으로 구성



(3) Query

  • 최상위 질의 클래스
    • TermQuery, BooleanQuery, PhraseQuery, PrefixQuery, TermRangeQuery, NumericRangeQuery, FilteredQuery 등이 Query를 상속받는다.



(4) TermQuery

  • 가장 기본이 되는 Query 구현 클래스

  • 특정 필드에 원하는 단어가 들어있는 Document를 검색할 때 사용한다.



(5) TopDocs

  • 검색결과 중 최상위 N개 Document에 대한 링크를 담고 있는 결과 클래스

  • 결과마다 각 Document의 docID(int타입)와 score(float타입)을 담고 있다.

    • docID를 사용해 실제 문서를 가져온다.

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

(루씬) 4 - 고급 검색 기법  (0) 2020.05.16
(루씬) 3 - 검색(Search)  (0) 2020.05.16
(루씬) 2 - 색인(Indexing)  (0) 2020.05.16

+ Recent posts