먼저 루씬은 참 쓰기 편하면서도 커스터마이징하기는 참 불편한 특징을 갖고 있습니다. (물론 제 수준상으로...)


그래서 루씬을 이용해서 쓰기 편하게(?) 나름대로 만들어봤습니다.


지금부터 루씬을 이용해서 기본적인 검색기를 뚝딱 만들어보겠습니다.


먼저 색인할 데이터를 다운 받습니다.


[트윗 데이터]


data.zip



위 파일은 JSON 형태의 트윗을 모아둔 파일로 압축을 풀면 약 45M 정도가 됩니다.


먼저 이 데이터를 원하시는 경로에 풀어둡니다.


저는 D:/tweet/ 에 압축을 풀겠습니다.


트윗을 담기 위한 객체를 만들어 둡니다.


객체 이름은 당연히 Tweet으로 하고 소스 내용은 아래와 같습니다.



package kr.peopleware.lucene.model;

import java.util.List;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericField;

public class Tweet extends BaseModel{
	private long tweetId;
	private String contents;
	private long createdAt;
	private long crawledAt;	
	private boolean linked;
	private List<String> urls;	
	public long getTweetId() {
		return tweetId;
	}
	public void setTweetId(long tweetId) {		
		this.tweetId = tweetId;
	}
	public String getContents() {
		return contents;
	}
	public void setContents(String contents) {
		this.contents = contents;
	}
	public long getCreatedAt() {
		return createdAt;
	}
	public void setCreatedAt(long createdAt) {
		this.createdAt = createdAt;
	}
	public long getCrawledAt() {
		return crawledAt;
	}
	public void setCrawledAt(long crawledAt) {
		this.crawledAt = crawledAt;
	}	
	public boolean isLinked() {
		return linked;
	}
	public void setLinked(boolean linked) {
		this.linked = linked;
	}
	public List<String> getUrls() {
		return urls;
	}
	public void setUrls(List<String> urls) {
		this.urls = urls;
	}
	@Override
	public Document convetDocument() {
		Document doc = new Document();
		NumericField createdAt = new NumericField("createdAt",Field.Store.YES,true);
		createdAt.setLongValue(this.getCreatedAt());		
		doc.add(createdAt);		
	
		
		NumericField tweetId = new NumericField("tweetId",Field.Store.YES,false);		
		tweetId.setLongValue(this.getTweetId());
		doc.add(tweetId);
		
		String type = "all";
		if(this.getUrls() == null || this.getUrls().size() == 0){
			type="link";
		}
		
		doc.add(new Field("type",type,Field.Store.NO,Field.Index.NOT_ANALYZED));
		if(this.getUrls() != null){
			doc.add(new Field("urls",this.getUrls().toString(),Field.Store.YES,Field.Index.NOT_ANALYZED));
		}		
		doc.add(new Field("contents", this.getContents(), Field.Store.YES, Field.Index.ANALYZED));
		return doc;
	}	
}


이때 BaseModel을 익스텐드 시켜서 사용했는데 BaseModel은 아래와 같습니다.


package kr.peopleware.lucene.model;

import org.apache.lucene.document.Document;

public abstract class BaseModel {
	public abstract Document convetDocument();
}

BaseModel은 Tweet이란 객체를 루씬 색인에 사용할 수 있게끔 Document로 변환시켜주는 역할을 합니다.


그럼 이제 데이터를 담아둘 객체는 생성이 끝났습니다.


이제 데이터를 가져오는 부분을 만들어 보겠습니다.


간단히 만들기 위해서 main 클래스 내에 getTweets라는 메소드를 만들겠습니다.


이 메소드는 주어진 경로에 따라서 데이터를 List 형태로 반환하는 역할을 합니다.





	private static List<Tweet> getTweets(String path) {
		List<Tweet> tweetList = new ArrayList<Tweet>();
		List<String> filenames = FileUtil.getFileNames(path);

		for (String filename : filenames) {
			List<String> lines = null;
			try {
				lines = FileUtil.load2List(filename);
			} catch (Exception e) {
				e.printStackTrace();
			}
			if(lines == null){
				continue;
			}
			for (String line : lines) {

				Tweet t = convertTweet((DBObject) JSON.parse(line));				

				tweetList.add(t);
			}
			//			System.out.println(filename);
		}
		return tweetList;
	}


여기서 주어진 path로부터 데이터를 한줄 한줄 가져오게 됩니다. 이 때 각 한줄 한줄을 Tweet이라는 객체로 변환 시켜야합니다. 그래서 convertTweet이라는 메소드를 생성하여 변환시켜줍니다. JSON 형태를 Tweet이라는 객체로 변환시켜주는 역할을 합니다. 그리고 JSON.parse는 몽고디비 라이브러리에서 제공하는 유틸리티입니다. 


convertTweet역시 귀찮기 때문에 main 클래스 내에 만들겠습니다.



	private static Tweet convertTweet(DBObject obj) {		
		Tweet tweet = new Tweet();
		tweet.setTweetId((Long) obj.get("tweetId"));
		tweet.setContents((String) obj.get("contents"));		
		tweet.setCreatedAt((Long) obj.get("createdAt"));
		tweet.setCrawledAt((Long) obj.get("crawledAt"));
		tweet.setLinked((Boolean)obj.get("linked"));
		@SuppressWarnings("unchecked")
		List<String> urls = (List<String>) obj.get("urls");
		if(urls != null){
			tweet.setUrls(urls);
		}				

		return tweet;
	}


그럼 여기까지 데이터를 가져와서 객체에 담는것 까진 완성이 되었습니다.


그럼 이제 실제 색인을 해보도록 하겠습니다. 


이 글에서는 루씬을 해부하는 것이 아니라 루씬을 이용한 검색기를 만드는 것이 중요하기 때문에 루씬 자체에 대한 사용방법은 이 글에서는 생략하겠습니다.


먼저 위에 getTweets와 convertTweet이 포함된 메인 클래스 입니다.



package kr.peopleware.lucene.index.test;

import java.util.ArrayList;
import java.util.List;

import kr.peopleware.lucene.index.Indexer;
import kr.peopleware.lucene.index.properties.PropertiesManager;
import kr.peopleware.lucene.model.Tweet;
import kr.peopleware.util.file.FileUtil;

import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.util.Version;

import com.mongodb.DBObject;
import com.mongodb.util.JSON;

public class IndexTester {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		PropertiesManager pm = new PropertiesManager("index.properties");
		Indexer indexer = new Indexer(new CJKAnalyzer(Version.LUCENE_36),"index.properties");		
		List<Tweet> tweetList = getTweets(pm.getProperty("TARGET_PATH"));			
		for (Tweet tweet : tweetList ) {
			indexer.index(tweet);
		}
		indexer.commit();
		indexer.close();
	}

	private static List<Tweet> getTweets(String path) {
		List<Tweet> tweetList = new ArrayList<Tweet>();
		List<String> filenames = FileUtil.getFileNames(path);

		for (String filename : filenames) {
			List<String> lines = null;
			try {
				lines = FileUtil.load2List(filename);
			} catch (Exception e) {
				e.printStackTrace();
			}
			if(lines == null){
				continue;
			}
			for (String line : lines) {

				Tweet t = convertTweet((DBObject) JSON.parse(line));				

				tweetList.add(t);
			}
			//			System.out.println(filename);
		}
		return tweetList;
	}
	private static Tweet convertTweet(DBObject obj) {		
		Tweet tweet = new Tweet();
		tweet.setTweetId((Long) obj.get("tweetId"));
		tweet.setContents((String) obj.get("contents"));		
		tweet.setCreatedAt((Long) obj.get("createdAt"));
		tweet.setCrawledAt((Long) obj.get("crawledAt"));
		tweet.setLinked((Boolean)obj.get("linked"));
		@SuppressWarnings("unchecked")
		List<String> urls = (List<String>) obj.get("urls");
		if(urls != null){
			tweet.setUrls(urls);
		}				

		return tweet;
	}

}


메인클래스에서 보면 "index.properties"라는 인자를 사용하게 됩니다.


이는 properties에서 색인 할 데이터의 경로, 색인이 저장될 경로등을 설정하기 위한 것입니다.


index.properties 파일 내용을 살펴보면




	SHUTDOWN	=	false	
	TARGET_PATH	=	D://tweet
	INDEX_PATH	=	D://tweetIndex
	
	#open mode (create,append,create_or_append)
	OPEN_MODE	=	create
	
	#merge setting
	MERGE_POLICY	=	true
	MAX_MERGE_AT_ONCE	=	100
	SEGMENTS_PER_TIER	=	100


아래 merge setting은 루씬과 직접적인 관계가 있는 설정입니다만, 기본적인 검색기를 만드는데는 그냥 저 값을 사용해도 무방합니다.


SHUTDOWN은 추후에 실시간 색인을 위한 옵션으로 현재는 사용하지 않습니다.


TARGET_PATH는 아까 data.zip의 압축을 푼 경로를 적습니다.


INDEX_PATH는 색인 데이터가 저장될 공간으로 원하는 곳에 경로를 적어주시면 됩니다.


OPEN_MODE는 색인기를 실행시에 기존 데이터를 새로 덮어쓸지(create), 기존 데이터에 추가 할 지(append)의 여부를 나타냅니다.


위 설정 파일의 설정을 마치면 이제 실행만 시키면 색인이 완료됩니다.




======================= 색인 끝 ======================




색인이 완료된 후 색인된 데이터를 검색해보도록 하겠습니다.


검색은 매우 심플합니다.



package kr.peopleware.lucene.search.test;

import java.util.Date;

import kr.peopleware.lucene.search.Searcher;
import kr.peopleware.lucene.util.QueryBuilder;

import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.util.Version;

public class SearchTester {

	/**
	 * @param args
	 */
	public static void main(String[] args) {	
		
		//설정 파일을 세팅하여 Searcher 생성
		Searcher search = new Searcher("search.properties");
		
//		search.setSort("tweetId",SortField.LONG,true);
		
		//색인된 데이터의 contents에 "감기"라는 검색어를 넣어서 검색
		Document[] docs = search.getDocuments(QueryBuilder.makeQuery("contents", "감기",new CJKAnalyzer(Version.LUCENE_36)));

		int count = 0;
		for (Document document : docs) {
			if(count == 10)break;
			System.out.println("["+count+"]");			
			System.out.println(document.get("urls"));
			System.out.println(document.get("tweetId"));
			Date date = new Date(Long.parseLong(document.get("createdAt")));
			System.out.println(date);
			System.out.println(document.get("contents"));
			System.out.println();
			count++;
		}
		System.out.println(docs.length);
		search.close();
	}

}


역시 search.properties란 파일을 사용하는데 search.properties에는 아래와 같이 색인 파일의 경로만이 저장되어있습니다.



	INDEX_PATH	=	D://tweetIndex


또한 QueryBuilder라는 static 메소드가 있는데 이는 질의어를 만들어주기 위한 메소드입니다. 


소스 내용은 아래와 같습니다만, 굳이 보실 필요는 없을 것 같습니다.



package kr.peopleware.lucene.util;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryTermVector;
import org.apache.lucene.search.TermQuery;

public class QueryBuilder {
 	public static Query makeQuery(String field,String q,Analyzer analyzer)
	{
		BooleanQuery bq = new BooleanQuery();
		QueryTermVector qtv = new QueryTermVector(q, analyzer);
		for(int i=0;i<qtv.size();i++)
		{
			bq.add(new TermQuery(new Term(field,qtv.getTerms()[i])), BooleanClause.Occur.MUST);
		}
		return bq;
	}
 	public static Query makeQuery(String field, Object a,Object b){
		Query nq = null;
		if(a.getClass() == Integer.class){
			nq = NumericRangeQuery.newIntRange(field, (Integer)a, (Integer)b, true, true);
			
		}else if(a.getClass() == Double.class){
			nq = NumericRangeQuery.newDoubleRange(field, (Double)a, (Double)b, true, true);
			
		}else if(a.getClass() == Float.class){
			nq = NumericRangeQuery.newFloatRange(field, (Float)a, (Float)b, true, true);
			
		}else if(a.getClass() == Long.class){
			nq = NumericRangeQuery.newLongRange(field, (Long)a, (Long)b, true, true);
			
		}
		return nq;
	}
}


이제 검색을 해보면 아래와 같은 결과를 보실 수 있습니다.





이클립스 프로젝트 파일도 함께 올려두겠습니다.


[루씬 프로젝트 파일]


lucene.zip


[루씬 프로젝트를 사용하는데 필요한 라이브러리]


common.zip



기본적인 검색 엔진은 이제 만들 수 있습니다만,


한글 검색 엔진은 "색인어 추출기"가 핵심이라고 말씀드릴 수 있습니다.


시간이 나는대로 형태소 분석기를 마무리 지어서 공개하도록 하겠습니다.

'Opensource > Lucene' 카테고리의 다른 글

루씬 기본적인 검색 엔진  (7) 2012.11.06
루씬 한글 검색 엔진 제작  (0) 2012.11.06
  1. darkly0828@naver.com 2013.01.15 14:59 신고

    글 잘 보았습니다. 많은 도움이 되네요~

  2. 안상준 2014.07.14 11:53 신고

    죄송하지만 Common page 를 어떻게 임포트 해야하는지 ;;;;;;;;;;;;;;;; 다른놈들은 에러없이 다 그냥 나오는데 kr.peopleware.util.file.FileUtil;
    이놈이 임포트 에러 뜨네요 폴더 자체를 Libarary 추가 해줫는데 해결이 안되네요 ㅠㅠ

    • shine_ing 2014.07.21 12:28 신고

      common도 별개의 프로젝트입니다. 그렇기 때문에 프로젝트 형식으로 import 해주시고 이클립스(를 사용하신다는 가정 하에)에서 빌드패스 부분에서 프로젝트를 통째로 잡아주시면 됩니다. 그래도 안되신다면 댓글 부탁드립니다~

  3. 2014.11.25 01:28

    비밀댓글입니다

  4. 아롱사태남 2014.11.25 01:29 신고

    안녕하세요 ^^ 요즘 오프소스에 관심이 많은 신입 개발자입니다.
    오픈소스 검색엔진에 대하여 검색하다가 루씬과 국내에서 개발한 패스트캣서치가 있던데
    신입인 저에게는 어떤걸로 먼저 뛰어드는게 좋을까요?
    루씬 단점은 한글 분석기가 지원이 안되고 초보자는 만들기 어렵다고 하더라구요 ㅜ
    패스트캣서치 단점은 국내에서 개발한지 별로되지않아 컴폼 받기가 힘들구요...
    사용해보시면서 루씬의 단점이라고 느겼던점과 장점을 가르쳐주셨으면 좋겠어요 그리고
    신입이라면 루씬을 도전해야할지 패스트캣서치를 도전해야할지 가르쳐주세요 !!

    • shine_ing 2014.11.25 09:51 신고

      저도 사실 루씬과 패스트캣 서치에 대해서 잘 알지는 못합니다만, 패스트캣서치가 루씬 베이스로 동작하기 때문에 아마 루씬을 먼저 보는게 낫지 않을까요? 그러나 업무상 적용할 용도라면 패스트캣서치가 훨씬 편리할 듯 보이네요

국내에서 루씬을 사용하는 검색 솔루션 업체가 상당히 많이 늘어나고 있습니다.


물론 분산 검색을 위해서 루씬을 포함하고 있는 Solr를 사용하는 업체도 늘어나고 있습니다.


물론 제가 루씬 검색 엔진을 제작한다고 하더라도 눈여겨볼 사람은 많지 않습니다.


단지 제가 하고 싶은건 검색 엔진의 이해가 아닌 검색 엔진의 활용 방안입니다.


검색 엔진을 이해하려면 사실 루씬의 색인 파일구조를 살펴봐야 합니다.


그러나 웬만한 실력이 아니고서야 ( 저를 포함 ) 그 어려운 소스를 뜯어서 파일을 분석해낸 후 자기 것으로 이해시키기에는 어려움이 있을 것입니다.


그래서 일단은 루씬을 이용해서 검색엔진을 만들어보고,


추후에 검색과 관련된 내용을 포스팅 할까 합니다.


제가 만들 내용은 "루씬을 이용한 실시간 분산 검색 엔진"입니다.


실제로 만드는 과정을 하나하나 포스팅 할테니 많은 지적부탁드립니다.

'Opensource > Lucene' 카테고리의 다른 글

루씬 기본적인 검색 엔진  (7) 2012.11.06
루씬 한글 검색 엔진 제작  (0) 2012.11.06

+ Recent posts