Cloudera CDH/CDP 및 Hadoop EcoSystem, Semantic IoT등의 개발/운영 기술을 정리합니다. gooper@gooper.com로 문의 주세요.
하둡 분산 파일 시스템을 기반으로 색인하고 검색하기
- 인터넷에서 바늘 찾기 -
원문: http://www.drdobbs.com/article/print?articleID=226300241
July 29, 2010
Kashyap Santoki는 Infosys Technologies Limited에서 일한다. KashyapChimanlal_S@infosys.com로 연락할 수 있다.
오늘날에는 정보가 넘치고 있다. 지역적으로 흩어져 있는 거대한 량의 정보는 계속해서 증가하고 있으며, 이러한 정보로부터 의미있는 결과를 얻어내려면 빠르게 파싱할 수 있는 시스템을 필요로 한다. 분산 데이터를 검색할 수 있는 색인을 만들 수 있다면, 파싱작업을 하는데 큰 도움이 된다. 이 글에서는 루씬(Lucene)와 자바를 사용해서 데이터를 색인하고 검색하는 기본적인 방법을 설명한다. 또한 RAMDirectory를 사용해서 색인하고 검색하는 방법, HDFS에 저장된 데이터에 대해 색인을 생성하는 방법, 이렇게 생성된 색인을 검색하는 방법을 설명한다. 마이크로소프트 윈도우 XP SP3을 플랫폼으로 사용하며, 개발환경으로는 자바 1.6, 이클립스 3.4.2, 루씬 2.4.0, 하둡 0.19.1으로 구성된다.
색인과 검색 작업을 위해서 하둡을 사용했다. 아파치 하둡 프로젝트는 안전하며, 확장 가능한 분산 컴퓨팅을 지원하는 오픈소스 소프트웨어다. 하둡 분산 파일 시스템(HDFS)는 원거리 네트워크상에서 파일을저장하고 공유하기 위한 목적으로 만들어졌다. HDFS는 일반적인 하드웨어상에서 동작할수 있도록 만들어졌다. 또한 장애 복구와 자원 관리 기능을 제공하며, 무엇보다도 응용 데이터 접근에 대한 높은 가용성을 지원한다.
로컬 파일 시스템에 색인 만들기
우선 로컬 파일 시스템에 저장된 데이터에 대한 색인을 만들어야 한다. 이클립스를 사용해서 먼저 프로젝트를 하나 생성하고, 클래스를 하나 만든다. 그리고 필요한 JAR 파일을 모두 프로젝트에 추가한다. 아래와 같이 일반적인 웹서버에서 생성하는 로그 파일을 데이터 예제로 사용하려고 한다.
2010-04-21 02:24:01 GET /blank 200 120
위 데이터는 아래와 같은 필드로 구성된다.
- 2010-04-21 -- 날짜 필드
- 02:24:01 -- 시간 필드
- GET -- 메서드 필드 (GET 또는 POST) -- 앞으로는 "cs-method"라는 용어로 사용하겠다
- /blank -- 요청 URL 필드 -- 앞으로는 "cs-uri"라는 용어로 부르겠다
- 200 -- 요청에 대한 상태 코드 -- "sc-status"라고 부르겠다
- 120 -- 처리 시간 필드 (요청을 처리하는데 걸린 시간)
샘플 파일은 "E:DataFile" 디렉토리에 "Test.txt"라는 이름으로 저장되어 있다고 가정한다. 샘플 파일의 내용ㅇ은 다음과 같다.
2010-04-21 02:24:01 GET /blank 200 120
2010-04-21 02:24:01 GET /US/registrationFrame 200 605
2010-04-21 02:24:02 GET /US/kids/boys 200 785
2010-04-21 02:24:02 POST /blank 304 56
2010-04-21 02:24:04 GET /blank 304 233
2010-04-21 02:24:04 GET /blank 500 567
2010-04-21 02:24:04 GET /blank 200 897
2010-04-21 02:24:04 POST /blank 200 567
2010-04-21 02:24:05 GET /US/search 200 658
2010-04-21 02:24:05 POST /US/shop 200 768
2010-04-21 02:24:05 GET /blank 200 347
"Test.txt" 파일에 저장된 데이터에 대해 색인을 만들어서, 색인을 로컬 파일 시스템에 저장하려고 한다. 아래의 자바 코드를 사용하면 이 작업을 할 수 있다. (각 코드가 어떤 작업을 처리하는지 주석을 상세히 달아 두었다).
// IndexWriter 객체를 생성한다. 이때 색인 파일을 저장할 경로를 인자로 전달한다.
IndexWriter indexWriter = new IndexWriter("E://DataFile/IndexFiles", new StandardAnalyzer(), true);
// BufferedReader 객체를 생성한다. 이때 색인하려고 하는 데이터가 저장된 파일 경로를 인자로 전달한다.
String row=null;
// 데이터 파일에서 각 라인을 읽는다.
while ((row=reader.readLine())!= null) {
// 한 행을 분할하여 각 필드를 구한 후, 배열에 저장한다. 데이터 파일에서 사용한 구분자는 공백 문자다
String Arow[] = row.split(" ");
// 각 행에 대해 document 객체를 생성한 후, 해당 document 객체에 필드를 데이터로 추가한다.
org.apache.lucene.document.Document document = new org.apache.lucene.document.Document();
document.add(new Field("date",Arow[0],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field("time",Arow[1],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("cs-method",Arow[2],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("cs-uri",Arow[3],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("sc-status",Arow[4],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("time-taken",Arow[5],Field.Store.YES,Field.Index.ANALYZED));
// document 객체를 색인 파일에 추가한다.
indexWriter.addDocument(document);
}
indexWriter.optimize();
indexWriter.close();
reader.close();
로컬 색인 파일 검색하기
이제 생성된 색인 파일에서 데이터를 검색할 수 있다. 기본적으로 검색은 "필드" 데이터 기반으로 수행된다. 루씬 검색 엔진에서 지원하는 다양한 검색 기능을 사용해서 검색할 수 있다. 또한 특정 필드 하나만을 사용할 수도 있고, 필드를 조합해서 검색할 수도 있다. 아래의 자바 코드는 색인을 검색하는 코드다.
// Searcher객체를 생성한다. 이때 인덱스 파일이 저장된 경로를 인자로 전달한다.
Searcher searcher = new IndexSearcher("E://DataFile/IndexFiles");
Analyzer analyzer = new StandardAnalyzer();
// 현재 색인 파일에 저장된 문서 또는 항목의 총 갯술르 출력한다.
System.out.println("Total Documents = "+searcher.maxDoc()) ;
// QueryParser 객체를 생성한다. 이때 검색을 적용할 필드명을 인자로 전달한다.
QueryParser parser = new QueryParser("cs-uri", analyzer);
//Query 객체를 생성한다. 이때 검색할 텍스트를 인자로 전달한다.
Query query = parser.parse("/blank");
// 아래의 문장을 실행하면 색인 파일에서 검색을 수행하게 된다.
Hits hits = searcher.search(query);
// 검색 질의문에 매칭하는 문서 또는 항목의 갯수를 출력한다.
System.out.println("Number of matching documents = "+ hits.length());
// 검색 조건에 매칭하는 문서(똔느 파일의 행)을 출력한다.
for (int i = 0; i < hits.length(); i++) {
Document doc = hits.doc(i);
System.out.println(doc.get("date")+" "+ doc.get("time")+ " "+
doc.get("cs-method")+ " "+ doc.get("cs-uri")+ " "+ doc.get("sc-status")+ " "+ doc.get("time-taken"));
}
이 예제에서는 cs-uri 필드에 대해, 해당 cs-uri 필드에 /blank가 포함된 텍스트를 검색했다. 따라서 검색 코드가 실행되면 cs-uri 필드에 /blank가 포함된 모든 문서(또는 행)이 결과로 출력된다. 출력은 아래와 같다.
Total Documents = 11
Number of matching documents = 7
2010-04-21 02:24:01 GET /blank 200 120
2010-04-21 02:24:02 POST /blank 304 56
2010-04-21 02:24:04 GET /blank 304 233
2010-04-21 02:24:04 GET /blank 500 567
2010-04-21 02:24:04 GET /blank 200 897
2010-04-21 02:24:04 POST /blank 200 567
2010-04-21 02:24:05 GET /blank 200 347
HDFS에서 메모리 기반으로 색인하기
이제 데이터가 HDFS와 같은 분산 파일 시스템에 저장된 경우를 생각해보자. 이처럼 데이터가 분산된 경우에는 앞에서 설명한 색인 파일을 바로 생성하는 코드는 제대로 동작하지 않는다. 따라서 색인 파일을 생성하기 전에 먼저 몇가지 작업을 우선 처리해야 한다. 예를 들어 HDFS에 저장된 데이터를 로컬 파일 시스템에 복사하는 작업, 로컬 파일 시스템에 해당 데이터에 대한 색인을 생성하는 작업, 마지막으로 색인 파일을 다시 HDFS에 저장하는 단계를 거쳐야 한다. 검색할 때도 비슷한 과정을 따라 처리해야 한다. 하지만 이러한 작업은 시간이 오래 걸릴뿐더러, 그리 좋은 방식이 아니다. 대신에 데이터가 저장되어 있는 HDFS 노드의 메모리를 사용해서 데이터를 색인하고 검색해보자.
앞에서 사용했던 데이터 파일인 "Test.txt"가 이제 HDFS의 "/DataFile/Test.txt" 경로에 저장되어 있다고 가정하자. 그리고 생성된 색인 파일을 저장할 디렉토리를 HDFS의 "/IndexFiles"에 생성하자. 아래의 자바 코드는 HDFS에 저장된 파일에 대해 색인 파일을 메모리에 생성하는 코드다.
// 인덱스 파일을 생성할 경로
String Index_DIR="/IndexFiles/";
// 데이터 파일이 저장된 경로
String File_DIR="/DataFile/test.txt";
// HDFS에 접근하기 위한 FileSystem 객체를 생성한다.
Configuration config = new Configuration();
config.set("fs.default.name","hdfs://127.0.0.1:9000/");
FileSystem dfs = FileSystem.get(config);
// 색인을 메모리에 생성할 RAMDirectory (메모리) 객체를 생성한다.
RAMDirectory rdir = new RAMDirectory();
// RAMDirectory 객체에 대한 IndexWriter 객체를 생성한다.
IndexWriter indexWriter = new IndexWriter (rdir, new StandardAnalyzer(), true);
// FSDataInputStream 객체를 생성한다. 이 객체를 사용해서 HDFS에 저장된 "Test.txt" 파일을 읽어들인다.
FSDataInputStream filereader = dfs.open(new Path(dfs.getWorkingDirectory()+ File_DIR));
String row=null;
// 파일에서 한 행씩 읽어들인다.
while ((row=reader.readLine())!=null) {
// 한 행을 분할하여 각 필드를 구한 후, 배열에 저장한다. 데이터 파일에서 사용한 구분자는 공백 문자다.
String Arow[]=row.split(" ");
// 각 행에 대해 document 객체를 생성한 후, 해당 document 객체에 필드를 데이터로 추가한다.
org.apache.lucene.document.Document document = new org.apache.lucene.document.Document();
document.add(new Field("date",Arow[0],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field("time",Arow[1],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("cs-method",Arow[2],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("cs-uri",Arow[3],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("sc-status",Arow[4],Field.Store.YES,Field.Index.ANALYZED));
document.add(new Field ("time-taken",Arow[5],Field.Store.YES,Field.Index.ANALYZED));
// document 객체를 색인 파일에 추가한다.
indexWriter.addDocument(document);
}
indexWriter.optimize();
indexWriter.close();
reader.close();
보다시피 "Test.txt" 파일은 HDFS에 저장되어 있으며, 이 파일에 대한 색인을 메모리에 생성했다. 색인 파일을 HDFS 디렉토리에 저장하려면.
// 메모리에 저장된 파일을 배열로 얻기
String fileList[]=rdir.list();
// 메모리로부터 인덱스를 읽어서 HDFS에 저장한다.
for (int i = 0; I < fileList.length; i++) {
IndexInput indxfile = rdir.openInput(fileList[i].trim());
long len = indxfile.length();
int len1 = (int) len;
// 파일로부터 바이트 배열로 데이터를 읽어들인다.
byte[] bytarr = new byte[len1];
indxfile.readBytes(bytarr, 0, len1);
// HDFS 디렉토리에 해당 인덱스 파일명을 가진 파일을 생성한다.
Path src = new Path(dfs.getWorkingDirectory()+Index_DIR+ fileList[i].trim());
dfs.createNewFile(src);
// 바이트 배열로부터 HDFS로 데이터를 쓴다.
FSDataOutputStream fs = dfs.create(new Path(dfs.getWorkingDirectory()+Index_DIR+fileList[i].trim()),true);
fs.write(bytarr);
fs.close();
}
dfs.closeAll();
이 작업을 통해 데이터 파일인 "Test.txt"에 필요한 색인 파일을 생성한 후, 생성된 색인 파일을 HDFS 디렉토리에 저장했다.
HDFS에서 메모리 기반으로 검색하기
이제 HDFS에 저장된 색인 파일에 대해 검색을 할 수 있다. 검색을 하려면 우선 HDFS에 저장된 색인 파일을 메모리로 올려야 한다. 아래의 코드를 보자.
// HDFS에 접근하기 위한 FileSystem 객체를 생성한다.
Configuration config = new Configuration();
config.set("fs.default.name","hdfs://127.0.0.1:9000/");
FileSystem dfs = FileSystem.get(config);
// 색인을 메모리에 생성할 RAMDirectory (메모리) 객체를 생성한다.
RAMDirectory rdir = new RAMDirectory();
// 해당 디렉토리에 저장된 색인의 목록을 얻어서 배열로 저장한다.
Path pth = new Path(dfs.getWorkingDirectory()+Index_DIR);
FileSystemDirectory fsdir = new FileSystemDirectory(dfs,pth,false,config);
String filelst[] = fsdir.list();
FSDataInputStream filereader = null;
for (int i = 0; i<filelst.length; i++) {
// HDFS 디렉토리에 저장된 색인 파일로부터 데이터를 filereader로 읽어들인다.
filereader = dfs.open(new Path(dfs.getWorkingDirectory()+Index_DIR+filelst[i]));
int size = filereader.available();
// 파일로부터 배열로 데이터를 읽어들인다.
byte[] bytarr = new byte[size];
filereader.read(bytarr, 0, size);
// RAMDirector에 파일을 생성한다. 이때 HDFS에 저장된 색인 파일의 이름과 동일한 이름으로 생성한다.
IndexOutput indxout = rdir.createOutput(filelst[i]);
// 바이트 배열로부터 RAMDirectory의 파일로 데이터를 쓴다.
indxout.writeBytes(bytarr,bytarr.length);
indxout.flush();
indxout.close();
}
filereader.close();
이제 필요한 색인 파일은 모두 RAMDirectory(또는 메모리)에 존재하게 된다. 따라서 해당 색인 파일에 대해 검색을 직접 실행할 수 있다. RAMDirectory에 대한 검색 코드는 로컬 파일 시스템에 대해 검색했던 코드와 유사하다. 유일한 차이점은 Searcher 객체를 생성할 때, 로컬 파일 시스템의 디렉토리 경로가 아니라 RAMDirectory 객체(rdir)를 사용해서 생성한다는 점이다.
Searcher searcher = new IndexSearcher(rdir);
Analyzer analyzer = new StandardAnalyzer();
System.out.println("Total Documents = "+searcher.maxDoc()) ;
QueryParser parser = new QueryParser("time", analyzer);
Query query = parser.parse("02\:24\:04");
Hits hits = searcher.search(query);
System.out.println("Number of matching documents = "+ hits.length());
for (int i = 0; i < hits.length(); i++) {
Document doc = hits.doc(i);
System.out.println(doc.get("date")+" "+ doc.get("time")+ " "+
doc.get("cs-method")+ " "+ doc.get("cs-uri")+ " "+ doc.get("sc-status")+ " "+ doc.get("time-taken"));
}
아래의 출력결과에서 보다시피, "time" 필드에 대해 "02:\:24\:04." 텍스트를 사용해서 검색을 수행한다. 따라서 코드가 실행되면, "time"에 "02:\:24\:04."가 포함된 문서(또는 행)이 결과로 출력된다.
Total Documents = 11
Number of matching documents = 4
2010-04-21 02:24:04 GET /blank 304 233
2010-04-21 02:24:04 GET /blank 500 567
2010-04-21 02:24:04 GET /blank 200 897
2010-04-21 02:24:04 POST /blank 200 567
결론
HDFS와 같은 분산 파일 시스템은 오늘날 만들어지는 방대한 량의 데이터를 저장하고 처리할 수 있는 막강한 도구다. 메모리 기반으로 색인하고 검색하는 방식을 사용하면, 산더미같은 데이터 중에서 정말로 원하는 데이터를 좀더 쉽게 찾을 수 있게 된다.