본문 바로가기
OpenSource/Lucene

[색인&검색] - 루씬 JUnit Test 해보기& RAMDirectory() 사용!

by 태하팍 2014. 6. 4.
반응형

우선! https://github.com/macluq/HelloLucene/blob/master/pom.xml

pom.xml을 보시면 루씬 core말고도 여러가지가 있습니다! 일단은 그냥 복사해서 붙여넣기를 해봅시다!

차근차근! 알아가보도록 하겠습니다^-^/

오키

사이트에 가서 보기 싫으신 분은 아래를 클릭!


우선은 아래와 같이 테스트 코드를 하나 만들어봅니다. 빨간색 부분이 무엇인지 파악해야 할 친구 입니다. ㅎㅎ

소스는 루씬 튜토리얼 소스라고 하네요 ver은 4.0 입니다!

https://github.com/macluq/HelloLucene


public class IndexTest {


@Test

public void makeIndexTest() {

   // 0. Specify the analyzer for tokenizing text.

   //    The same analyzer should be used for indexing and searching

   StandardAnalyzer analyzer = new StandardAnalyzer(Version.LUCENE_40);

}


StandardAnalyzer 이 친구는 아래와 같이 lucene-analyzers-common에 포함이 되어있습니다.

무엇을 하는 친구인지~~api를 살펴보겠습니다.^-^/

http://lucene.apache.org/core/4_0_0/analyzers-common/





위의 그림을 보시면 StopwordAnalyzerBase를 extends 받아서 구현을 한 것을 알 수 있습니다.

음..이부분을 이해하려면 먼저 색인에 대해서 조금 더 학습을 해야 할 것 같습니다.

용어를 대충 보면 Tokenizer, Filter 등등이 보이네요 그리고 Stopword가 확~~눈에 띄이네요 불용어?? 

그러면 생각해보면...

무엇을 Tokenizer하고 Filter한다는 걸까요????

제가 보기에는...수집(Crawler)되어 나온 SCD(전형화된 문서)를 Parsing 하고 그것을 형태소분석을 통해 색인파일을 만드는 것 같습니다.

제가 생각하는 것이 맞는지는 소스를 차근차근 따라해보면 알 수 있을 것 같네요 ㅎㅎㅎ 

테스트를 하면서 정리를 하는 것이라서..끝까지 읽어주세요 ㅋㅋㅋ;;;

방구뽕



2012/08/28 - [OpenSource/Lucene] - [Study_1회차] Lucene이란??


에 보시면..아래와 같이 주요 클래스들이 있습니다. 오래된 루씬 책에서 발췌한 것이라 틀릴수도있겠지만 검색엔진의 틀은 변하지 않을테니

그냥 요정도로 봐도 무관 할 것 같습니다. ㅎㅎㅎ


색인 주요 클래스

검색 주요 클래스 

 IndexWriter

IndexSearcher

 Directory

 Term

 Analyzer

 Query

 Document

 TermQuery

 Field

 Hits


 색인 주요 클래스에 대해서 알아보도록 하겠습니다. 

1) IndexWriter : 이 친구를 사용해야 새로운 색인을 만들 수 있고, 색인에 문서를 추가하는 작업을 합니다.

                        즉, 루씬의 색인에 문서를 추가하는 기능을 전담하는 친구입죠! 소중한 친구네요 ㅎㅎ; 더욱 친해져야겠어요ㅋㅋ

2) Directory : 색인도 저장이 되어야 합니다. 그래서 이 친구의 역할은 루씬의 색인 파일 저장을 책임 집니다.

                    루씬은 기본적으로 RAMDirectory와 FSDirectory를 지원 합니다.

                    ㄴ  RAMDirectory : 컴퓨터의 메인 메모리를 색인 저장소로 사용 합니다.

                    ㄴ FSDirectory     : 디스크의 파일 시스템에 색인을 저장 합니다.

                   위의 2가지는 기본적으로 제공되어지는 것이며 DB를 색인저장소로 할 수도있습니다. 

                   하둡 교육을 들으면서 검색 생각이 났었습니다. 하둡같은 분산저장소에 수집 또는 색인 내용을 저장하지 않을까..라는 생각을 해보네요

                   혹은..Redis같은곳에 올릴수 있지 않을까요?? 음..해보고 싶은 것 해봐야할 것들이 생각나네요+ㅁ+/ 

                   아무튼..차근차근 해보도록 하겠습니다! ^-^/ 재밌는 개발 ㄱ ㄱ ㄱ ~~


3) Analyzer : 루씬은 문자열 즉, 일반 텍스트만을 색인하며, 분석하도록 지정한 모든 텍스트는 분석기(Analyzer)를 거칩니다.

                   색인이라는 것이 형태소분석기를 통해 나온 파일 이기때문이죠! 여기서 텍스트라는 것은 분석할 대상인 것 입니다.

                   예를 들어서 텍스트 즉, 수집된 내용이 <title>박태하 천재</titie> 에서 파싱을 통해 텍스트를 뽑아내 박태하 천재를 

                   형태소분석(ex. 복합명사 추출)을 해보면 박태하, 천재, 박태하 천재 이런식으로 분석기를 통해 뽑아 낼수 있습니다.


4) Document : 루씬은 색인에 추가할 데이터는 모두 문서, 즉 Document 단위로 처리 합니다.

                      간단히 말해 루씬의 색인에 데이터를 추가하는 방법은 Document 클래스를 사용하는 방법밖에 없다는 말입니다.


5) Field         : IndexWriter를 통해 색인에 추가하려는 내용은 Document 인스턴스에 Field의 형태로 추가해야 한다.

                      검색을 할경우에도 어느 필드에서 찾을지 지정하도록 되어있다.

                      4가지의 필드를 제공한다.

                         ㄴ Keyword, UnIndexed, UnStored, Text

 필드 생성 메소드

분석 여부 

 검색 가능 여부 

저장 여부 

 Field.Keyword(String, String)

N

 Y

 Y

 Field.Keyword(String, Date)

N

Y

 Y

 Field.UnIndexed(String, String)

 N 

 N 

 Y

 Field.UnStored(String, String)

 Y

 Y

 N

 Field.Text(String, String)

 Y

 Y

 Y

 Field.Text(String, Reader)

 Y

 Y

 N


     수집 된 어느 내용들(ex. title, contents, date, name, id) 중에 어떤 Field들을 색인 또는 검색 대상으로 잡아야할지를 결정해주는 역할을 한다.


우리가 테스트 해볼 소스는 인덱스 및 검색까지 하는 소스 입니다. 그러므로 검색(Search) 부분도 살펴보도록 하겠습니다. 

 검색 주요 클래스

1) IndexSearcher : IndexWriter로 만들어둔 색인file을 이용해 검색하려면 이 친구를 이용하면 됩니다! 

                            이 친구는 검색만을 담당하기 때문에 색인을 '읽기 전용' 으로 사용 합니다. 즉, 검색 중에도 IndexWriter를 통해 

                            색인에 문서를 얼마든지 추가할 수 있습니다.

                           가장 간단한 검색메소드는 질의(Query)를 입력 받아 결과를 도출하는 메소드 입니다.


2) Term : 텀..이 친구는 색인의 내부에서 단어를 의미하는 가장 기본적인 요소 입니다.

                하나의 텀은 하나의 이름과 하나의 단어로 이루어지며, 색인 과정에서 Analyzer가 분석해준 토큰을 이용해

                IndexerWriter가 텀을 만들어낸다. 색인파일 내부구조에서 사용되어짐을 알 수있다.  

                예를들어 어떤 단어를 검색하려고 했을 때, 다음과 같이 텀을 하나 만들어서 TermQuery로 IndexSearcher에서 

               검색할 수 있다.

                Query q = new TermQuery(new Term("contents", "parktaeha"));

                Hits hits = is.search(q);          

                위의 소스를 잠시보면 "parktaeha" 요녀석이 검색어가 되겠다. 

                &q=%EB%B0%95%ED%83%9C%ED%95%98 이런 형식으로다가 올 것이다. 

                그리고 contents라는 필드에 "parktaeha"라는 단어가 포함하는 모든 문서를 검색하라는뜻이다.


3) Query :  IndexSearcher를 통해 색인을 검색하려면 검색어가 필요하다. 루씬에서 검색어를 지정할 때는 Query를 사용한다.

                 차근차근 더 학습하면서 알아보도록 하겠습니다!


4) TermQuery : TermQuery는 루씬이 지원하는 Query의 하위 클래스 중 다른 모든 것들의 기본인 클래스이다.

                        이 친구는 특정 이름의 필드에 지정한 단어가 포함되어 있는 문서를 찾을 때 사용한다.


5) Hits : IndexSearcher에서 Query를 통해 검색한 결과는 Hits로 수집할 수 있다. Hits는 결과문서의 본문 내용을 

               모두 포함하지는 않으며, 결과 문서에 대한 ID 목록만을 갖는다. 즉, 필요할 때 결과에 해당하는 문서를 가져오는게 

               훨씬 효과적이기 때문이다.


여기까지 이론적으로 살펴 보았습니다.

소스를 직접 보고 고쳐보면서 더욱 더 자세히 보겠습니다.

Junit으로 돌리기는 하지만 유닛테스트는 아닙니다..^^;;; 우선은 설계를 하지 않고 그냥 소스 가져온것을 돌려보겠습니다.

lucene 패키지를 하나 만드시고~아래의 소스를 돌려보도록 하죠! 

package lucene;

import static org.junit.Assert.*;


import java.io.IOException;


import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.document.StringField;

import org.apache.lucene.document.TextField;

import org.apache.lucene.index.CorruptIndexException;

import org.apache.lucene.index.DirectoryReader;

import org.apache.lucene.index.IndexReader;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.IndexWriterConfig;

import org.apache.lucene.queryparser.classic.QueryParser;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.Query;

import org.apache.lucene.search.ScoreDoc;

import org.apache.lucene.search.TopScoreDocCollector;

import org.apache.lucene.store.Directory;

import org.apache.lucene.store.LockObtainFailedException;

import org.apache.lucene.store.RAMDirectory;

import org.apache.lucene.util.Version;

import org.junit.Test;


public class IndexTest {


@Test

public void makeIndexTest() throws CorruptIndexException, LockObtainFailedException, IOException {

// 0. Specify the analyzer for tokenizing text.

//    The same analyzer should be used for indexing and searching

StandardAnalyzer analyzer = new StandardAnalyzer(Version.LUCENE_40);


// 1. create the index

Directory index = new RAMDirectory();


IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_40, analyzer);


IndexWriter w = new IndexWriter(index, config);

addDoc(w, "Lucene in Action", "193398817");

addDoc(w, "Lucene for Dummies", "55320055Z");

addDoc(w, "Managing Gigabytes", "55063554A");

addDoc(w, "The Art of Computer Science", "9900333X");

w.close();


// 2. query

String querystr = "Lucene";


// the "title" arg specifies the default field to use

// when no field is explicitly specified in the query.

Query q = null;

try {

q = new QueryParser(Version.LUCENE_40, "title", analyzer).parse(querystr);

} catch (org.apache.lucene.queryparser.classic.ParseException e) {

e.printStackTrace();

}


// 3. search

int hitsPerPage = 10;

IndexReader reader = DirectoryReader.open(index);

IndexSearcher searcher = new IndexSearcher(reader);

TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage, true);

searcher.search(q, collector);

ScoreDoc[] hits = collector.topDocs().scoreDocs;


// 4. display results

System.out.println("Found " + hits.length + " hits.");

for (int i = 0; i < hits.length; ++i) {

int docId = hits[i].doc;

Document d = searcher.doc(docId);

System.out.println((i + 1) + ". " + d.get("title") + "\t" + d.get("title"));

}


// reader can only be closed when there

// is no need to access the documents any more.

reader.close();

}


private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {

Document doc = new Document();

doc.add(new TextField("title", title, Field.Store.YES));


// use a string field for isbn because we don't want it tokenized

doc.add(new StringField("isbn", isbn, Field.Store.YES));

w.addDocument(doc);

}

}


디렉토리 구조 - 아래와 같이 IndexTest.java를 Junit  기반으로 만들어 줍니다.



          


결과는 아래와 같습니다.


  

이제~~대망의 소스를 보도록 하겠습니다!!


소스를 그냥 보는것이 아니라main쪽으로 소스를 분리 해보겠습니다.  

1) 테스트 소스

package lucene;

import org.apache.lucene.search.Query;

import org.junit.Assert;

import org.junit.Test;


import kr.pe.constr.lucene.Indexer;


public class IndexTest {

@Test

   public void makeIndexerTest(){

      Indexer idx = new Indexer();

      Query q = idx.queryExcute();

      Assert.assertNotNull(idx);

      Assert.assertNotNull(q);

      System.out.println("query=>"+q.toString());

      idx.search(q);

    }

}


2) 메인쪽 소스 - 예외처리 요런것은 일단 패스~~

package kr.pe.constr.lucene;

import java.io.IOException;


import org.apache.lucene.analysis.standard.StandardAnalyzer;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.document.StringField;

import org.apache.lucene.document.TextField;

import org.apache.lucene.index.CorruptIndexException;

import org.apache.lucene.index.DirectoryReader;

import org.apache.lucene.index.IndexReader;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.IndexWriterConfig;

import org.apache.lucene.queryparser.classic.QueryParser;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.Query;

import org.apache.lucene.search.ScoreDoc;

import org.apache.lucene.search.TopScoreDocCollector;

import org.apache.lucene.store.Directory;

import org.apache.lucene.store.RAMDirectory;

import org.apache.lucene.util.Version;


public class Indexer {

private StandardAnalyzer analyzer;

private Directory index;

// Step 01.

public Indexer(){

IndexWriter w = null;

IndexWriterConfig config; 

try {

// The same analyzer should be used for indexing and searching

analyzer = new StandardAnalyzer(Version.LUCENE_40);


// 1. create the index

index = new RAMDirectory();

config = new IndexWriterConfig(Version.LUCENE_40, analyzer);

w = new IndexWriter(index, config);

addDoc(w, "Lucene in Action", "193398817");

addDoc(w, "Lucene for Dummies", "55320055Z");

addDoc(w, "Managing Gigabytes", "55063554A");

addDoc(w, "The Art of Computer Science", "9900333X");

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}finally{

try {

w.close();

} catch (CorruptIndexException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}


}

private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {

Document doc = new Document();

doc.add(new TextField("title", title, Field.Store.YES));


// use a string field for isbn because we don't want it tokenized

doc.add(new StringField("isbn", isbn, Field.Store.YES));

w.addDocument(doc);

}

// step 02.

public Query queryExcute(){

// 2. query

String querystr = "Lucene";


// the "title" arg specifies the default field to use

// when no field is explicitly specified in the query.

Query q = null;

try {

q = new QueryParser(Version.LUCENE_40, "title", analyzer).parse(querystr);

} catch (org.apache.lucene.queryparser.classic.ParseException e) {

e.printStackTrace();

}

return q;

}

// step 03.

public void search(Query q){

// 3. search

int hitsPerPage = 10;

IndexReader reader = null;

ScoreDoc[] hits = null; 

IndexSearcher searcher;

TopScoreDocCollector collector;

try {

reader = DirectoryReader.open(index);

searcher = new IndexSearcher(reader);

collector = TopScoreDocCollector.create(hitsPerPage, true);

searcher.search(q, collector);

hits = collector.topDocs().scoreDocs;

display(hits, searcher);

} catch (CorruptIndexException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}finally{

try {

reader.close();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

private void display(ScoreDoc[] hits, IndexSearcher searcher){

// 4. display results

System.out.println("Found " + hits.length + " hits.");

for (int i = 0; i < hits.length; ++i) {

int docId = hits[i].doc;

Document d;

try {

d = searcher.doc(docId);

System.out.println((i + 1) + ". " + d.get("title") + "\t" + d.get("isbn"));

} catch (CorruptIndexException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}


}


다시 JUnit을 돌려봤을 때 아래와 같이 녹색불이 뜨며 결과는 같아야 하겠죵~!




결과 


이제 소스는 색인과 검색 둘다 있지만 우선! 색인에 대해서 알아보도록 합니다~


TEST소스에서 Indexer idx = new Indexer();를 수행하면 생성자가 발동하겠죠!


색인을 생성하는 부분 입니다. 굳이 생성자에서 할 필요는 없죵~


        // Step 01.

public Indexer(){

IndexWriter w = null;

IndexWriterConfig config; 

try {

// The same analyzer should be used for indexing and searching

analyzer = new StandardAnalyzer(Version.LUCENE_40);

                       // Filters StandardTokenizer with StandardFilter, LowerCaseFilter and StopFilter, using a list of English stop words.


// 1. create the index

index = new RAMDirectory();

config = new IndexWriterConfig(Version.LUCENE_40, analyzer);

w = new IndexWriter(index, config);

addDoc(w, "Lucene in Action", "193398817");

addDoc(w, "Lucene for Dummies", "55320055Z");

addDoc(w, "Managing Gigabytes", "55063554A");

addDoc(w, "The Art of Computer Science", "9900333X");

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}finally{

try {

w.close();

} catch (CorruptIndexException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}


} 


// 색인에 추가할 데이터 Document단위로 넣어주기 

private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {

Document doc = new Document();

System.out.println("title->"+title+"=== isbn->"+isbn);

doc.add(new TextField("title", title, Field.Store.YES));


// use a string field for isbn because we don't want it tokenized

doc.add(new StringField("isbn", isbn, Field.Store.YES));

w.addDocument(doc);

}



// step 02. 질의분석!!

public Query queryExcute(){

// 2. query

String querystr = "Lucene";


// the "title" arg specifies the default field to use

// when no field is explicitly specified in the query.

Query q = null;

try {

q = new QueryParser(Version.LUCENE_40, "title", analyzer).parse(querystr); // 질의 분석

System.out.println("q="+q);

} catch (org.apache.lucene.queryparser.classic.ParseException e) {

e.printStackTrace();

}

return q;

} 



// step 03. 색인을 가져와 검색하기 

public void search(Query q){

// 3. search

int hitsPerPage = 10;

IndexReader reader = null;

ScoreDoc[] hits = null; 

IndexSearcher searcher;

TopScoreDocCollector collector;

try {

reader = DirectoryReader.open(index);  // index reading

searcher = new IndexSearcher(reader);  // Creates a searcher searching the provided index.

collector = TopScoreDocCollector.create(hitsPerPage, true); // 어떻게 가져올 것인가? 

searcher.search(q, collector); // 색인에 대해 search(), Lower-level search API. return void

hits = collector.topDocs().scoreDocs;  // docId를 꺼내 hits[]에 넣어준다. 

display(hits, searcher);

} catch (CorruptIndexException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}finally{

try {

reader.close();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

} 


// 출력하기 

private void display(ScoreDoc[] hits, IndexSearcher searcher){

// 4. display results

System.out.println("Found " + hits.length + " hits.");

for (int i = 0; i < hits.length; ++i) {

int docId = hits[i].doc;  // return docId number

System.out.println("docId="+docId);

Document d;

try {

d = searcher.doc(docId);  // return 실제 문서 document object return

System.out.println((i + 1) + ". " + d.get("title") + "\t" + d.get("isbn"));

} catch (CorruptIndexException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

} 


주석으로 설명을 하기는 하였으나..더욱 더 자세히 알아보시려면 직접 api를 찾아가면서 테스트를 해보시는게 좋을 것 같습니다^-^


여기까지~~~해보고 다음편으로! 색인 저장소를 파일시스템으로 바꿔보겠습니다. 배웠으니 이제는 익힐 차례!!! ㅎㅎㅎ 

다음편에 뵙겠습니다 (- - (_ _ * 꾸벅~


소스는 github에 올려놨습니다~

https://github.com/acetaeha/rndStartService/tree/luceneBasicSource01



참고 URL :

http://lucene.apache.org/core/4_0_0/

http://lucene.apache.org/core/4_0_0/core/org/apache/lucene/search/class-use/ScoreDoc.html

http://lucene.apache.org/core/4_0_0/core/org/apache/lucene/search/IndexSearcher.html#doc(int)

http://lucene.apache.org/core/4_0_0/core/org/apache/lucene/search/Collector.html

http://lucene.apache.org/core/4_0_0/core/org/apache/lucene/index/IndexReader.html

http://lucene.apache.org/core/4_0_0/queryparser/org/apache/lucene/queryparser/classic/QueryParser.html

http://lucene.apache.org/core/4_0_0/core/org/apache/lucene/document/StringField.html

http://lucene.apache.org/core/4_0_0/core/org/apache/lucene/document/TextField.html

http://lucene.apache.org/core/4_0_0/analyzers-common/

http://lucene.apache.org/core/


                         - END -

반응형