## 목차 ##
## 1. 루씬이란? ##
검색 기능을 제공하는 라이브러리(IR 라이브러리)
- 검색 애플리케이션이 아닌 색인과 검색 기능을 제공하는 도구이다.
- 아파치 재단의 오픈소스 프로젝트
- Java를 기본으로 C++, C, Perl 등으로 포팅 되어있다.
간결하고 강력하다.
- JAR파일 크기 1MB
색인과 검색 기능만 제공
- 문서 수집, 텍스트 추출, 관리 등은 프로그래머가 직접 처리해야한다.
## 2. 루씬과 검색 애플리케이션의 구조 ##
- 검색 애플리케이션의 구조
- 원문 분석 및 처리하여 색인 형식에 맞는 문서(루씬 문서) 생성
- 문서 텍스트 분석
- 색인에 문서 추가
- 사용자가 질의 생성
- 질의 실행하여 색인에서 문서 검색
- 결과 출력
(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
중요한 점은 색인에 문서가 반영되는 시점은 IndexWriter
의 close()
메소드가 호출될 때이다. 혹은 IndexWriter
의 commit()
메소드를 실행하면 닫지 않고도 반영할 수 있다.
(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 |