[pylucene] Python용 lucene검색엔진에 한글형태소 분석기 lucenekorean 붙이기 Develop Tip

검색 엔진을 처음 생각한 것은 90년대 초 였습니다.
국내 모 전화번호부 검색 시스템에서 "짜장면"을 검색 단어로 입력하면,
"중국집"을 검색하라는 비디오텍스 프로젝트의 서브 프로젝트로 GTS (Guide To Service)
프로젝트를 맨땅에서 하면서 부터였습니다.
그 이후에는 여러 오픈 소스의 검색 엔진도 여럿 나왔습니다.
그 중, 현재 가장 활발하게 사용되고 있는 엔진은 루씬 (Lucene) 일 것입니다. 
최근에는 lucene 을 두 번 정도 프로젝트에 사용해 보았고, 5년 정도가 흘렀습니다.
그동안 버전 1.xx 부터 사용하여 벌써 3.xx 로 버전업이 되었고,
가장 큰 변화는 우리가 apache 웹서버, 톰캣 WAS 서버 등으로 잘 사용하는
암튼 세상에 아주 많은 사람들이 루씬이라는 검색엔진을 이용해서 다양한 프로젝트 및 
제품에 넣고 있는데 국내에는 의외로 그 이용 빈도가 적은 것이 사실이지요.
그곳에는 "한글 형태소 분석기"가 큰 걸림돌 일 것입니다.
우리나라에 "한글 형태소 분석기"를 제일 처음 개발하신 분은 아마도
강 교수님과 국립국어연구원 프로젝트를 95년도에 진행하면서
한글 형태소 분석 라이브러리를 이용하기도 했었는데,
십 몇년간 이상이나 오픈소스로 이용할 만한 한글 형태소 분석기가 없었나 봅니다.
(뭐 있었다면 제가 그동안 잘 몰라 왔었던 것으로 이해해 주세요~. 계속 검색 쪽만 해 온 것이 아니어서리..)
그러다가 루씬의 형태소 분석의 일환으로 되어 있는 lucenekorean 소스포지 프로젝트가 있음을 알았습니다.
또한 네이버 카페이도 "루씬 한글 분석기 오픈소스 프로젝트"라고 있다는 것을 알았지요.
그럼 이 한글 오픈소스를 이용하여 java로 개발하는데 사용할 수 있습니다.
자~ 그런데 저는 요즘 주 언어로 Python을 이용하다보니,
루씬이 파이썬으로 포팅된 것이 우선 있는지 찾았으나 그것은 없고,
pylucene 라고 해서 이 것 역시 아파치 lucene의 같은 레벨에 개발되고 있더군요.
물론 C#용으로는 lucene.net 이라고 있고, clucene 라고 해서 C로 포팅되고 있는 프로젝트 들도 있으나
모두 아파치에서 정식으로 지원되는 것은 아닌 것으로 알고 있습니다.
(하지만 lucene.net 으로도 프로젝트를 잘 해서 윈도우 내에서 검색엔진으로 잘 사용되고 있습니다)
그런데 pylucene 을 사용하면 되는데, 문제는 한글 형태소 분석기에서 시작됩니다.
한글 형태소 분석기를 pylucene에 붙이고 싶다는 마음이 생겼지요.
구글에서 찾아본 바, 성공했다고 응답하신 분이 없어서
이틀간 시간을 들인 끝에 우분투 (10.10)에서 성공하였기에,
그 방법을 올려 봅니다.
가장 어려웠던 부분은 pylucene은 JCC라는 것을 이용한다는 것입니다.
이해하기로는 java의 jar 패키지를 읽어 public 심볼을 자동으로 C++ 래핑 소스로 자동생성하고,
그것을 shread library로 만들어 놓은 다음 JNI를 이용해서 python 프로세스 내에서 바로
java 호출을 한다는 것인데, 여기에서 기존의 KoreanAnanlyzer 가 오류가 발생한다는 부분이었습니다.
다음과 같은 순서에 따라 작업을 하여 성공하였습니다.
일단 우분투 데스크탑 10.10 이라고 가정하고 pydev 개발 환경 과 동일한 방법으로 pydev 개발 환경이 갖추어 졌다고 가정합니다.
일단 소스포지에 가서 svn이나 cvs 로 터미널에서 CheckOut 시키려고 했으나 이상하게 그 방법으로는 되지 않더군요.
일단  ~/tmpkl 이라고 빈 곳에 이클립스의 워크스페이스를 하나 만듧니다. (없는 디렉터리를 넣고 OK)
이클립스에서 상단 메뉴의 Window>Open Perspective>Others... 를 선택합니다.
첫번째의 "CVS Repository Exploring..."을 선택하고 OK 합니다.
상단같이 New>Repository Location을 누릅니다.
카페 대문에 가면 위와 같이 CVS 주소가 나옵니다.
Host에   ":pserver:anonymous@lucenekorean.cvs.sourceforge.net:/cvsroot/lucenekorean" 를 넣으면
자동으로 상단 처럼 보입니다. "Finish" 합니다.
헤더 안에 있는 kr.analyzer 라는 프로젝트를 선택해서 마우스 오른쪽 단추의 "Check Out..." 합니다.
kr.morph 프로젝트도 체크아웃 합니다. (두 개의 프로젝트만 있으면 됩니다)
다시 "Java Perspective"를 누르면,
일단 kr.analyzer 프로젝트가 오류가 보입니다.
kr.analyzer의 Property를 선택하고 (alt+Enter 를 눌러도 됩니다)
"Java build path"를 선택한 후, Project 탭의 "kr.morph" 프로젝트를 선택하고 "OK" 해 줍니다.
그럼 위와 같이 오류가 없이 경고만 나오는 것을 알 수 있습니다. (경고는 deprecate method를 호출하였거나 아니면 
과거 버전의 java 문법을 사용해서 나오는 것인데 다 수정할 수는 없고 그냥 둡니다..^^)
또한 이 글을 쓰는 시점 (2010년 12월 16일)에서 가장 마지막으로 적용된 lucene 버전이 2.9.1 임을 위에서 알 수 있습니다.
(카페에서 3.x는 소스 수정 중이시라고 나오는 군요)
이제 중요한 부분인데, pylucene build를 위해서 소스부터 작업을 하다가 발생한 오류 부분을 수정할 부분입니다.
위와 같이 kr.morph 프로젝트의 org.apache.lucene.analysis.kr.utils 패키지에 있는 VerbUtil.java를 열고,
verbSuffix 를 선택합니다.
선택된 상태에서 Refactor>Rename을 눌러,
s_verbSuffix 라고 수정한 후, 엔터 합니다.
결국 오류가 난 상황은 위와 같이 외부에 노출되는 동일한 이름의 함수가 있어, 신택스 상으로 JCC를 거쳐 C++ 로 변환할
때 오류가 난 상황입니다. (java와 C++ 상의 신택스 검사 수준의 차이라고나 할 까요~)
다음에는 jar 파일을 만들기 위하여,
kr.analyzer 프로젝트에서 src 폴더에서 마우스 오른쪽 단추를 눌러 "Export..."를 선택합니다.
Java>Jar를 선택하고 "Next>"를 누릅니다.
kr.analyzer 뿐만 아니라 kr.morph 프로젝트의 src 폴더만 각각 선택하고 JAR file의 "Browse..." 단추를 누릅니다.
~/tmpkl 폴더에 "pyKoreanAnalyzer-2.9.1.jar" (뒤에 jar 까지 넣어야 합니다) 라 입력하고 "확인" 합니다.
다시 Export 상의 "Finish"를 누르면,
상단과 같이 경고는 뜨지만 역시 무시합니다. 그리고 OK.
이제 pylucene 2.9.1을 다운받기 위하여,
위와 같이 브라우저에서 받거나, 혹은  pylucene-2.9.1-1-src.tar.gz 링크로 다운로드 받습니다.
(~/tmpkl 폴더에 "pyKoreanAnalyzer-2.9.1.jar"와 같은 위치에 다운로드 합니다.)
터미널 창을 열고, ~/tmpkl 로 들어가 받은 pylucene 파일을 
$ tar xvfz pylucene*.tar.gz
명령으로 압축 해제 합니다.
시스템에 g++이 설치되어 있어야 하는데,
$ sudo apt-get install g++
로 없으면 설치를 합니다.
$ cd ~/tmpkl/pylucene-2.9.1-1/jcc 
위의 디렉터리로 이동 후에
$ python setup.py build
합니다. 이것은 jcc 시스템을 build 하는 부분입니다.
(OSX인 경우, $ ARCHFLAGS="-arch i386 -arch x86_64" python setup.py build)
build가 끝난 후에는 
$ sudo python setup.py install
을 수행하여 시스템에 설치를 하게 됩니다.
/usr/local/lib/python2.6/dist-packages 디렉터리에 JCC egg 정보와 jcc 아래에 설치되었음을 알 수 있습니다.
이제는 
$ cd ~/tmpkl/pylucene-2.9.1-1
로 돌아와서, pylucene build를 위해서
$ vi Makefile
Makefile을 수정합니다.
기존에 해당되는 것이 없으므로,
121번째 줄 정도에 위와 같이,
# Linux     (Ubuntu 10.10 32-bit, Python 2.6.6, OpenJDK 1.6)
PREFIX_PYTHON=/usr
ANT=ant
PYTHON=$(PREFIX_PYTHON)/bin/python
JCC=$(PYTHON) -m jcc.__main__
NUM_FILES=2
KOREANANALYZER_JAR=$(PYLUCENE)/../pyKoreanAnalyzer-2.9.1.jar
(OSX 10.6 인 경우 아래와 같이 추가합니다)
# Mac OS X  (Python 2.6.1, Java 1.6.0_24, setuptools 0.x, Intel Mac OS X 10.6)
PREFIX_PYTHON=/usr
ANT=ant
PYTHON=$(PREFIX_PYTHON)/bin/python
JCC=$(PYTHON) /Library/Python/2.6/site-packages/JCC-2.4.1-py2.6-macosx-10.6-universal.egg/jcc/__init__.py
NUM_FILES=2
KOREANANALYZER_JAR=$(PYLUCENE)/../pyKoreanAnalyzer-2.9.1.jar
와 같은 내용을 추가하고,
JARS 선언부분 아래에 위와 같이,
JARS+=$(KOREANANALYZER_JAR)
라는 내용을 추가합니다. 
저장하고 닫은 후에, make 합니다.
(OSX인 경우 $ ARCHFLAGS="-arch i386 -arch x86_64" make)

위와 같이 이상 없이 "build of complete" 가 나오면 정상 끝입니다. (아마 십여분 기다릴 수도 있습니다)
위와 같이 
$ sudo make install
하여 설치를 합니다.
/usr/local/lib/python2.6/dist-packages 디렉터리에 lucene egg 정보와 lucene 아래에 설치되었음을 알 수 있습니다.
이렇게 해서 설치가 무사히 끝났습니다.
혹시 우분투에 자체 lucene lib를 가지고 있다고 하더라도 위와 같이 별도의 2.9.1을 갖는 pylucene이 도는 것을 알아야 합니다.
그럼 java로 테스트 했던 간단한 코드,
//import static org.junit.Assert.assertEquals;
//import java.io.IOException;
import junit.framework.TestCase;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.kr.KoreanAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
//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.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
/-*
 * A very simple demo used in the API documentation (src/java/overview.html).
 *
 * Please try to keep src/java/overview.html up-to-date when making changes
 * to this class.
 *-
public class TestDemo extends TestCase {
        public void testSimple() throws Exception {
                //Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);
                Analyzer analyzer = new KoreanAnalyzer();
                // Store the index in memory:
                Directory directory = new RAMDirectory();
                // To store an index on disk, use this instead:
                //Directory directory = FSDirectory.open("/tmp/testindex");
                IndexWriter iwriter = new IndexWriter(directory, analyzer, true,
                                new IndexWriter.MaxFieldLength(25000));
                Document doc = new Document();
                String text = "This is the text to be indexed. 이것은 아버지가 방에 들어가신다";
                doc.add(new Field("fieldname", text, Field.Store.YES, Field.Index.ANALYZED));
                iwriter.addDocument(doc);
                iwriter.close();
                // Now search the index:
                IndexSearcher isearcher = new IndexSearcher(directory, true); // read--nly=true
                // Parse a simple query that searches for "text":
                QueryParser parser = new QueryParser("fieldname", analyzer);
                Query query = parser.parse("text");
                ScoreDoc[] hits = isearcher.search(query, null, 1000).scoreDocs;
                assertEquals(1, hits.length);
                // Iterate through the results:
                for (int i = 0; i < hits.length; i++) {
                        Document hitDoc = isearcher.doc(hits[i].doc);
                        //assertEquals("This is the text to be indexed.", hitDoc.get("fieldname"));
                        System.out.printf("[%04d]: %s\n", i+1, hitDoc.get("fieldname"));
                }
                isearcher.close();
                IndexReader ireader = IndexReader.open(directory, false); // read--nly=false
                TermEnum tenum = ireader.terms(new Term("fieldname", ""));
                for (int i = 1;tenum.term() != null && tenum.term().field() == "fieldname"; ++i) {
                        System.out.printf("[%04d]===> <%s>\n", i, tenum.term().text());
                        tenum.next();
                }
                ireader.clone();
                directory.close();
        }
}
위의 내용이 파이썬으로는 ...
# -*- coding: utf8 -*-
import time
import os
import lucene
def demoTest():
        lucene.initVM(lucene.CLASSPATH)
        analyzer = lucene.KoreanAnalyzer();
        directory = lucene.RAMDirectory()
        # writer
        writer = lucene.IndexWriter(directory, analyzer)
        doc = lucene.Document()
        text = "This is the text to be indexed. 이것은 아버지가 방에 들어가신다";
        doc.add(lucene.Field("fieldname", text, lucene.Field.Store.YES, lucene.Field.Index.ANALYZED));
        writer.addDocument(doc);
        writer.close();
        # Now search the index:
        searcher = lucene.IndexSearcher(directory)
        # Parse a simple query that searches for "text":
        parser = lucene.QueryParser('fieldname', analyzer)
        query = parser.parse("text");
        hits = searcher.search(query)
        # Iterate through the results:
        i = 0
        for h in hits:
                i += 1
                doc = lucene.Hit.cast_(h).getDocument()
                print '[%04d]: %s' % (i, doc.get("fieldname"))
        searcher.close()
        # get all terms from all index
        ireader = lucene.IndexReader.open(directory, False)
        term = lucene.Term('fieldname', '')
        termenum = ireader.terms(term)
        term = termenum.term()
        i = 0
        while term and term.field() == 'fieldname':
                i += 1
                print "[%04d]===> <%s> " % (i, term.text())
                term = termenum.next() and termenum.term()
        ireader.close();
        directory.close();
        pass
if __name__ == '__main__':
        demoTest()
으로 거의 동일하게 수행하여, (파이썬 코드가 더 간편함을 알 수 있다)
위와 같이 pydev 환경에서 잘 수행되었다.
참고로, 위에서 build 했던 
파일을 올려본다. 가급적이면 lucenekorean 프로젝트에 반영되었으면 좋겠네요~

핑백

  • 지훈현서 : [우분투,데비안] 패키지 만들기 2010-12-21 20:25:25 #

    ... 이 앞전에 포스팅 했었던, pylucene 한글 형태소 분석기용 만들기 에서와 같이 만든 것을 우분투나 데비안에서는 패키지로 만들 수 있습니다.(deb 확장자로 만들어짐) 아래와 같이 간단한 방법으로 만들 수 있 ... more

  • 지훈현서 : [Ubuntu, OSX] Python에서 Java 클래스 연동 JPype 2011-04-21 12:21:34 #

    ... 파이썬에서 Java 클래스를 호출하여 사용하는 방법을 찾아보았습니다.일전에 pyLucene을 살펴보면서 나온 JCC는 C++ 언어를 소스차원에서 Java JNI 인터페이스로 변경하여 파이썬의 Wrapping 작업을 거쳐java를 호출하는 ... more

  • 지훈현서 : [Python] 검색엔진 Whoosh 2013-08-05 15:56:37 #

    ... 는 별도의 검색엔진라이브러리가 없어 루씬 엔진에 인터페이스를 한 pyLucene 이라는 것을이용하고는 했습니다.(참고: Python용 lucene검색엔진에 한글형태소 분석기 lucenekorean 붙이기) 그러다가 비교적 최근에 나온 파이썬 검색엔진 라이브러리로Whoosh 라는 것을 찾게 되었습니다. 2009년도 부터 시작 ... more

덧글

  • 짐승 2011/01/17 16:00 # 삭제 답글

    안녕하세요. 좋은 자료 읽고 갑니다.
    국내에서 루씬을 다루시는 분들이 많지 않은 관계로 이러한 자료 하나하나가 다른분들에게 소중할거 같네요.
    좋은 하루되세요! :D
  • 지훈현서아빠 2011/01/17 17:11 #

    넵~ 도움 되셨다면 저의 작은 기쁨입니다.
    감사합니다 ^^
  • fehead 2011/09/15 18:21 # 삭제 답글

    질문하나 들입니다.

    파이루씬을 이용하여 프로그램을 만들어서 다른 PC에서 설치해서 사용할 계획이 있는데요
    프로그램을 개발할때는 java를 깔아서 프로그램을 만드는걸로 아는데요.

    만약 다른 PC에서 사용할때 다른 PC에 java를 설치 하지 않고 사용할수 있는 방법이 있는지 궁금합니다.
  • 지훈현서아빠 2011/09/15 19:10 #

    pylucene가 설치된 위치 (예, /usr/local/lib/python2.7/dist-packages/lucene/*)
    를 확인하였더니 _lucene.so (아마도 JCC로 만들어진 인터페이스 파일 및 십여개의 *.jar 파일들이 있더군요)
    즉, jar가 실제로 구동하여야 한다는 의미이므로 JVM 환경에서 수행이 된다고 봐야겠지요~
  • fehead 2011/09/16 09:18 # 삭제 답글

    답글을 달아 주셔서 감사합니다.
    결국 안되는거였군요.
    자바를 쓰던가 아니면 clucene을 쓰는수밖에 없겠네요.
댓글 입력 영역

구글애드텍스트