루씬으로 뭔가 작업을 한다면 사실 가장 어려운 부분이 Analyzer를 입맛에 맞게 만드는 부분일 것 입니다.
지금까지 작성한 Analyzer에 대한 내용이 이렇게 길었던 것도
그런 이유였고요...

이제부터 이야기 할 Indexing, Search, Query, Sort등은 사실 사용하기가
그렇게 어려운 것은 아닙니다. 이미 API자체가 너무나도 잘 만들어졌기 때문에
그냥 특별한 옵션 없이 사용하는 것 만으로도 충분히 잘 사용 할 수가 있습니다.

그 다음엔 사실 경험이구요... 포스트의 내용도 기본적인 API에 대한 테스트케이스와 
이것들을 사용하면서 기억에 남았던 내용들에 대한 것을 소개해드리는 내용이 될 것 같습니다. 

사실 대부분의 내용은 JAVADOC이나 루씬인액션과 같은 책을 읽어보는 것이
훨씬 도움이 될 것 입니다. 

그래서 이번부터는 간단하게 TestCase를 작성하여
API가 어떻게 사용되는지 보여드리고 필수적은 옵션들에 대해서만
설명을 하려고 합니다. 

색인 할 때 사용하는 IndexWriter나 검색시 사용하는 IndexSearcher등은 실제로 이를 사용하는 개발자가
어떻게 사용할까의 문제이지 이 API자체를 사용하기는 어렵지 않습니다.

우선 IndexWriterTestCase를 보겠습니다. 이 테스트케이스는 Lucene in Action에 있는 
예제를 조금 수정한 예제입니다.

 
IndexWriterTest.java

예전의 API와는 다르게 최근 버전의 루씬의 IndexWriterIndexWriterConfig라는 객체를 받아서 생성하도록
되어있습니다. 그외 나머지 생성자들은 거의 대부분 deprecated 처리가 되었죠.. 4.0이 되면 아마
정리를 하지 않을까 생각됩니다.

색인을 하기 위한 클래스가 IndexWriter입니다. 이 클래스는 Document를 받아 색인파일을 작성하는 클래스입니다.
내부적으로는 색인파일을 읽기 위한 IndexReader 클래스를 가지고 있습니다.

색인파일을 작성하기위해 필요한 것이 Directory인데요 루씬에서는 몇가지 종류의 디렉토리를 제공하고
있습니다. 위 예제는 RAMDirectory로 JVM메모리에 공간을 만들어 이곳에 색인파일을 작성합니다.
당연히 JVM이 내려가면 색인 내용도 전부 사라집니다. 하지만 속도가 빠르기 때문에 작은 용량의 색인 파일을
메모리에 올려놓고 사용 할 경우에는 유용하게 사용됩니다.

앞선 포스트중에서 동의어필터를 개발 할 때 이 RAMDirectory를 사용하였었습니다.

그 다음에 제공되는 것이 FSDirectory입니다.
이 클래스는 실제로 물리적인 하드에 색인 파일을 작성합니다.
사용법은 아래와 같습니다.

Directory directory = FSDirectory.open(new File("d:/index"));


내부적으로는 더 많은 종류의 Directory가 있지만 일단 크게 이렇게 두가지로 보시면 될 것 같습니다.

이렇게 만들어진 Directory를 사용하여 IndexWriter를 생성합니다.

IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter indexWriter = new IndexWriter(directory, conf);


IndexWriterConfig에는 많은 설정을 할 수 있는데 그 중 필수 설정이 바로 Analyzer의 설정입니다. 
이렇게 설정된 Analyzer에 의해서 Document의 각 필드의 내용을 Token으로 분석하여 색인합니다.

위 경우에는 문서의 내용을 WhitespaceAnalyzer를 사용하여 색인하고 있습니다.

기본적으로 공백을 기준으로 Token을 만들어 색인이 될 것이라고 생각 할 수 있습니다.

나중에 다시 한번 언급을 하겠지만 색인에서 사용되는 Analyzer와 검색에서 검색어를 분석 할 때 사용되는
Analyzer는 기본적으로 같은 것을 사용합니다.

"나는 개발자다" 라는 문장에 대해서 색인 할 때는 "나는+개발자다" 라고 분석하여 색인하고
검색어 분석에 사용되는 Analyzer에서는 "나"+"개발자"라고 분석한다면 위 문장은
절대로 검색이 되지 않을 것 이기 때문입니다.

그 후 실제로 Document를 생성하고 indexWriter.addDocument(Document d) 메서드를 통해서
Document를 색인합니다.
이때 Document를 생성 할 때 사용되는 것이 Field 클래스입니다.

Document는 여러개의 Field로 이루어져있고 이 Field는 이름과 값을 가지고 있습니다.
루씬의 스킴은 기본적으로 자유스킴(schema-free)이기 때문에 하나의 Document에 같은 이름을 가진 Field가 여러개
존재해도 상관이 없고, 하나의 IndexWriter를 이용하여 색인 할 때 사용되는 Document들이
모두 같은 Field를 가지고 있을 필요도 없습니다.

Field를 생성하는 부분을 보면

new Field("ids", ids[i], Field.Store.YES, Field.Index.NOT_ANALYZED)


로 되어있습니다.

맨 앞의 파라메터는 이름, 두번째는 값입니다.
3,4번째의 파라메터가 중요한데요 Field.Store.YES는 이 값을 색인 파일에서
가지고 있을 것이냐를 결정합니다. 이것이 YES로 설정이 되면 IndexSearcher를 통해 검색 된 Document에서
이 필드의 값을 가져 올 수 있습니다. 물론 그만큼 색인파일의 사이즈는 커지겠죠... 보통 검색결과 페이지에서
화면에 보여 줄 내용들을 이 옵션을 통해서 처리합니다.

4번째 파라메터는 이 필드의 값을 색인 할 것이냐와 분석 할 것이냐를 결정합니다.
5가지 옵션이 존재하는데요.. 그 중 3가지에 대해서만 간단히 설명 드리겠습니다.

Field.Index.NOT_ANALYZED
Field.Index.ANALYZED
Field.Index.NO


NOT_ANALYZED는 색인은 하되 분석은 하지 않는 다는 것입니다. 즉, 위 예제에서 WhitespaceAnalyzer를 통해
문장을 Token으로 쪼개지 않고 통채로 색인을 하겠다는 것 입니다. 이 구문은 보통 숫자나 PK값등 분석이 필요없는 필드에 많이 사용됩니다.

Analyzed는 위와 반대로 분석을 하겠다는 것이구요...

NO는 색인을 하지 않겠다는 것 입니다.

그외 2가지 옵션은 여기서 설명하기는 다소 양이 많으므로 책을 보시기를 추천합니다. ^^

맨 마지막으로는 commit을 하고 close를 합니다.

IndexWriter를 통해 변경된 색인 파일의 내용은 IndexWritercommit을 해야 실제 색인파일에 반영이 됩니다.
이 단순한 한줄에도 많은 이야기들이 들어있는데.. 일단 IndexReaderIndexSearcher
IndexWrtier에 의해서 최종 commit된 내용만 볼 수 있다는 것을 알아두셨으면 합니다. (예외도 있습니다..)

위 예제 중 testDeleteDocument 메서드를 보시면 indexWriter를 통해서 ids 1번 문서를 삭제하고 있습니다.
그리고 commit을 하지 않고 다시 문서수를 체크하여보니 여전이 3개로 나오죠..
하지만 commit을 하고 체크를 하니 2개의 문서를 가지고 있는 것으로 나옵니다.

추가적으로 testDeleteAndOptimizeDocument 메서드를 보시면
numDocsmaxDoc 메서드의 차이를 보여주고 있습니다.
실제로 IndexWriter를 통해서 document를 삭제하면 바로 실제 색인파일에서 document
삭제되는 것은 아닙니다. commit을 하더라도 document에는 삭제된 document라는 마킹만 되어
있는 상태인 것이죠... numdocmaxdoc은 그 차이이고 색인파일 최적화(optimize)는 이러한 마킹 된 document들을
실제로 삭제 처리하게 됩니다.

그리고 IndexWriter는 색인파일을 열때 lock 메커니즘으로 동작을 합니다.
하나의 색인 파일에 대해서는 한번에 하나의 IndexWriter만이 생성 될 수 있습니다.

예를 들어서 아래와 같은 경우에는
org.apache.lucene.store.LockObtainFailedException 이 발생합니다.


ndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter indexWriter = new IndexWriter(directory, conf);

IndexWriter indexWriter2 = new IndexWriter(directory, conf);


indexWriter를 열고 close하지 않은 상태에서
indexWriter2를 생성하려고 하면서 에러가 발생하게 됩니다.

IndexWriter 그리고 DocumentField에 대한 이야기는 책으로도 한 챕터가 나올만큼 굉장히 방대한 양입니다. 

때문에 앞서 말씀드린대로 포스트에서는 기본이 되는 내용에 대해서만 언급을 하였습니다.

하지만 위 정도의 내용과 테스트 케이스의 소스만 사용하셔도
기본적인 색인과 테스트들은 해보 실 수 있을 것 이라 생각됩니다.

내용이 좀 부실 한 것 같습니다.. 하지만 색인을 한다면 이정도로도 충분히 색인이 가능 할 것이라 생각되고요..
저 같은 경우도 IndexWriter의 사용에 있어서는 Document들의 설계나 IndexWriter를 생성하고 닫는 구현의
방법만 조금 달리 할 뿐 기본적으로 위 예제에서 크게 다르지는 않습니다. 

위 테스트 케이스는

https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다. 
Posted by 용식
이번에는 많은 분들께서 기다리셨을(?) 명사 추출 필터입니다.
사전에 등록되어있는 명사를 기반으로 Tokenizer나 선행 TokenFilter로 부터 넘어온
Token을 탐색하여 명사를 추출해내는 구조입니다.

기대를 하셨다면 죄송스럽게도 굉장히 별거 없는 Filter입니다.
 
형태소 분석이 아닌 단순히 Token을 읽어 사전에 있는 단어를 추출하는 방식입니다.
Token탐색도 그냥 character단위로 쭉.... 합니다.

이걸 다른 자료구조형으로 만들면 속도를 빠르게 할 수도 있겠지만
여기서는 코드의 간결함을 위해서 복잡한 부분은 모두 제외를 시킬 예정입니다.

이 추출의 과정이 형태소분석을 기반으로 알고리즘이 만들어져 있다면 이 Filter가 형태소분석기가 됩니다.
하지만 이 형태소분석이라는 것이 쉬운 것이 아니고..요즘은 웹이 처음 태동 할 당시처럼 형태소 분석으로
명사만 추출해서 되는 상황도 아닙니다. 때문에, 여기서는 사전을 기반으로 Token을 탐색하여
매칭되는 명사들을 그냥 추출해는 정도로 구현을 하려고 합니다.

그리고 가장 중요한 것이 사전으로 형태소분석을 하던 지금과 같은 방식을 사용하던 이 사전은 계속 유지보수가 되어야합니다.
신조어가 계속 생겨나기 때문입니다.

때문에 기본이 되는 사전을 가지고 핵심적인 모듈을 구현하고 그외 신조어/불용어/동의어등 
사용자가 직접 커스터마이즈 할 수 있는 사전을 제공하여 그 rule을 추가적으로 적용하는 방법들도 많이 사용됩니다.

그럼 이 Filter의 테스트케이스부터 보겠습니다.

 

DevysNounFilterTest.java

우선 추출의 대상이 되는 문장이 있고 여기서 추출되어야 하는 단어들을 미리 List에 넣어두고
추출되는 단어들을 비교합니다. 동의어필터 테스트케이스와 마찬가지로 양방향으로 검증을 합니다.

명사가 추출되는 것이 보이시나요?

비록 아주 단순한 로직이지만 이렇게 문장으로부터 원하는 키워드가 추출되기 시작하면 뭔가 기분이 좋아집니다. :)

그럼 Filter의 코드를 보겠습니다.

 
DevysNounEngine.java
DevysNounFilter.java

기본적으로는 동의어필터의 SynonymEngine과 같은 인터페이스를 구현하고있습니다.
하나의 Token으로부터 여러개의 Token이 파생되는 경우에는 대부분 Stack<AttributeSource>로 활용이
가능하기 때문에 이를 이용해서 인터페이스를 만들고 이용합니다.

내부로직은 각 엔진이 가지고 있게 됩니다.

Token을 하나씩 인덱스를 증가시켜가며 단어를 추출하고
이를 사전과 비교하여 사전에 있는 단어라면 이를 Stack에 저장해 넘겨줍니다.

여기서 word 타입만을 명사 추출 대상으로 삼는데
동의어의 경우 기본적으로 명사를 추출 할 대상이 아니게 되고 기타 불필요한 로직을 타지 않게 하기 위합니다.
따라서 동의어필터와 명사필터의 우선순위를 봤을 때 추출된 명사의 동의어를
추출해야하므로 명사필터가 동의어필터보다 앞에 와야함을 알 수 있습니다.

사전/탐색/비교등의 로직을 더 빠르고 확실하게 구현한다면 그래도 꽤 쓸만한 명사 추출 필터가 되지 않을까 합니다.
다만 여러방법으로 명사를 추출하도록 하는 것 예를들면 사전과 매칭되는 가장 긴 명사를 추출한다던가 하는 부분이 추가되면
더 좋을 것 같습니다.

이 필터에서 한가지 중요한 것은 원문 토큰도 리턴을 해준다는 것 입니다.

즉 , "하둡을" 이라는 Token에서 "하둡"뿐만이 아니라 
"하둡"+"하둡을"Token이 추출되는 것 입니다.

이는 나중에 사용자가 "하둡을 사용하는 방법"이라고 검색을 했다고 하면
기본적으로 사용자가 입력한 키워드에 더 많은 가중치를 두어서
검색 결과의 우선순위를 높이는데 사용하기 위함입니다.

"하둡을" * BOOST! + "하둡" + "사용하는" + "방법" 와 같은 형태의 쿼리를 사용 할 수도 있을 것 입니다.

또한 위 로직에서는 빠져있지만 추출된 명사의 OFFSET을 제대로 계산하여
Attribute에 설정해주어야 합니다. 나중에 매치된 키워드에 하이라이팅을 해주기 위해서는
OFFSET정보가 필요하기 때문입니다.

이렇게해서 루씬을 사용한 개발에서 가장 중요하고 어려운 부분 중 하나인
Analyzer에 대한 이야기를 우선 마칠 수 있을 것 같습니다. 어렵고 중요한 부분인데 설명이나 코드의 부족함이
많았던 것 같아서 아쉬움이 남기도 하고 걱정도 됩니다.

공유해드린 소스를 사용하여 이런저런 테스트를 해보신다면
더 쉽게 Analyzer에 접근 하실 수 있을 것이라 생각됩니다.

이후에는 색인과 검색, 그리고 쿼리등에 대해서 간단한 코드를 사용하여
설명을 드리려고 합니다.

아직 코드를 작성해두지 않아 조금 시간이 걸릴지도 모르겠네요..^^
 
https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다. 


Posted by 용식
이번에는 동의어 필터를 만들어보겠습니다. 동의어의 역할은 다 아시겠지만 특정한 키워드에 대해서 확장된 검색을 지원 할 수 있도록 해줍니다. 이 예제도 앞선 예제들과 마찬가지로 루씬인액션의 책에 있는 예제를 보고 우리 상황에 맞춰 다시 만든 코드들입니다. 먼저 테스트 케이스를 보겠습니다. DevysSynonymFilterTest.java
테스트 케이스는 양방향으로 검사를 하고 있습니다. 
동의어 목록을 미리 지정해두고 TokenFilter로부터 나오는 Token을 리스트와 비교.. 
그리고 이 Token을 따로 저장해두고 다시 동의어 목록을 가지고 비교합니다. 

 예를 들어서 동의어가 "노트북, notebook, 노트북PC"로 등록되어 있다면 3개 중 하나의 키워드가 
검색쿼리로 들어온다고 해도 동의어로 등록된 3개의 키워드 전부로 검색이 되는 효과를 줄 수 있습니다. 
이걸로 어느정도 오타에 대한 커버도 가능하겠죠. 보통 동의어 사전을 저런식으로 모두 동등한 키워드로 
등록하는 것과 노트북:notebook, 노트북PC 이런식으로 대표어를 설정하여 등록하는 방식 두가지를 
생각해볼 수 있겠습니다. 

관리적인 면에서는 후자가 편하겠지만 확장성에서는 전자가 편하다고 생각되므로 
전자의 방식으로 동의어 필터를 구현하겠습니다. 

이 동의어의 확장을 검색시에 수행 할 것이냐 색인시에 수행 할 것이냐도 
조금 생각 해 볼 만한 문제입니다. 검색시에 확장을 하게 된다면 "노트북"이라는 검색 쿼리에 대해서 
실제로 검색엔진에서 "노트북" or "notebook" or "노트북PC"와 같은 형태로 쿼리 자체가 확장 될 수도 있겠습니다. 

하지만 루씬에서의 동의어 검색은 위와 같은 방법이 아니라 색인시에 동의어를 사용하여 같이 색인을 하고 동의어가 어떤 것이 들어오던 검색이 가능하도록 해주고 있습니다. 

특히 이 소스에서는 동의어를 루씬 RamDirectory를 사용 색인하여 두고 활용하기 때문에 동의어들에 대해서 어느 동의어가 들어오더라도 똑같은 기능을 하도록 구현하였습니다. 기본적인 컨셉은 이렇습니다. 

 루씬에서 하나의 Document에는 같은 이름의 필드를 여러개 등록 할 수 있습니다. 
이것을 활용하여 루씬의 RamDirectory를 사용하여 색인하여 활용 하려고 합니다. 
동의어 사전이 위와 같이 ROW단위로 등록이 되어 있다고 할 때 각 ROW를 읽어 ","로 split을 한 후 하나의 Document에 동일 필드명(syn)으로 집어 넣어 루씬의 RamDirectory를 활용하여 색인합니다. 

 그리고 검색 쿼리가 들어 올 경우 그 쿼리로 색인된 동의어 리스트에서 검색을 하여 나온 Document에서 syn필드의 값들을 꺼내 그것으로 검색을 하도록 하는 것 입니다. 


대부분의 사전을 이렇게 색인을 활용 할 수도 있을 것 입니다. 
이렇게 추출된 동의어의 리스트를 실제 필터에서 어떻게 활용하는지 보겠습니다. 
이번에 만들어질 동의어 필터는 SynonymEngine 이라는 클래스를 내부적으로 사용하고 있습니다. 
 이 SynonymEngine은 Engine 인터페이스를 상속한 것으로써 Stack를 리턴해주는 getAttributeSources 메서드를 하나 가지고 있습니다. 
 이 SynonymEngine은 위 예제처럼 색인과 검색을 통하여 동의어의 리스트를 받아와서 이를 AttributeSource에 저장하여 이것의 State를 Stack에 저장합니다.


DevysSynonymFilter.java
이제 State에 대한 이야기를 해야 할 것 같습니다.

필터 중 이렇게 Tokenizer로부터 추출 된 하나의 Token으로부터 여러개의 Token을 추가적으로 추출해내는 필터들이 있습니다. 
 동의어 필터라던가 명사 추출 필터 같은 경우 하나의 Token으로부터 추가적인 Token들을 추출해내죠. 
 이런 경우에 필터는 원본 Token을 저장해두고 추가적으로 추출된 Token을 Collection 혹은 Stack에 넣어두고 그 Token들을 리턴한 후 맨 마지막에 원본 Token을 리턴하여 작업을 끝내는 방식을 사용 할 수 있습니다. 
(보통 한번 사용 된 Token은 사라져야 하기 때문에 Stack등이 많이 사용됩니다.) 

 이때 사용되는 것이 State입니다. 

 2.X에서 3.X로 넘어오면서 Token의 각 속성에 대해 Attribute라는 클래스가 생긴 것과 이 State가 생긴 것이 Analyzer의 내부 구현에서 가장 큰 변화라고 생각합니다. 
 처음에 이 개념을 이해하는데 조금 애를 먹었는데요 공부했던 것이 오래전이라서 저도 다시금 기억을 되살려가며 설명 드리려 합니다. 

 앞에서 언급했지만 Analyzer의 Tokenizer와 모든 TokenFilter들은 TokenStream 클래스를 상속하고 있습니다. 
 이 TokenStream클래스는 내부적으로 AttributeSource 클래스를 상속받고 있고 이 AttributeSource 클래스가 이전 Token에서의 속성들인 CharTermAttribute등 여러 종류의 Attribute들을 가지고 있는 구조입니다. 

 때문에 데코레이터패턴으로 물려있는 Tokenizer와 TokenFilter들이 모두 이 속성을 공유하여 작업 할 수 있는 것 입니다. 그렇다면 만약 A라는 단어로부터 B,C,D라는 동의어가 추출된다고 할 때 예전 같으면 이 추출된 키워드를 가진 Token 클래스를 다음 Filter에게 넘겨주면 되는 구조였지만, 

이제는 B,C,D 각 동의어들의 속성을 가진 AttributeSource를 만들어내어 다음 Filter가 사용 할 수 있도록 해줘야 하는 것 입니다. 

하지만 3.X의 루씬에서 incrementToken 메서드는 더 이상 Token을 리턴해주는 것이 아니라 단순히 boolean 값을 리턴해줍니다. 위에서 몇번 언급을 하였듯이 TokenSteam들은 AttributeSource를 공유하여 사용하는 구조입니다.

여기서 바로 AttributeSource의 captureState와 restoreState 메서드가 사용됩니다. 

기본적으로 추출된 키워드를 포함한 Attribute들이 모두 AttributeSource에 들어있는 것이기 때문에 이 AttributeSource 자체를 저장해뒀다가 꺼내서 사용하는 개념입니다. 

선행 Tokenizer나 TokenFilter로부터 넘어온(실제로는 AttributeSource에 저장해둔) AttributeSource를 복사하여 이것을 가지고 동의어에 대한 속성을 만들어서 Stack에 그 State를 저장하여 둡니다. 왜 복사를 하냐면 우리는 원본에 대한 정보도 필요하기 때문입니다. 원본에 대해서 뭔가 조작을 해버리면 원본 속성을 잃어버리기 때문에 복사를해서 작업을 하는 구조로 되어있는 것 입니다.
SynonymFilter의 incrementToken 메서드를 보면

synonyms = engine.getAttributeSources(input.cloneAttributes());


이런 부분이 있습니다. 동의어를 추출 할 SynonymEngine에게 현재 Token의 정보를 가지고 있는 AttributeSource를 복사하여 넘겨주고 있습니다. 그러면 SynonymEngine에서는 동의어 추출을 위해 필요한 정보들을 이 AttributeSource로부터 얻어 올 수 있습니다.
SynonymEngine에서는 이 복사된 AttributeSource를 사용하여 동의어에 대한 속성들을 Set한 후 이 AttributeSource의 State를 Stack에 저장합니다.

synonymStack.push(attributeSource.captureState());


그리고 incrementToken 메서드에서는 Stack의 size를 체크하여 Stack이 채워져있으면 그 Stack으로부터 State를 받아와서 현재 TokenStream의 AttributeSource를 Stack에 넣어졌던 State(동의어 정보)로 변경합니다.

restoreState(synState);


그리고 true를 리턴하지요. 그러면 후행 TokenStream에서는 추출 된 동의어에 대해서 작업이 가능합니다.

물론 incrementToken 메서드를 보시면 Stack에 비어있는 경우 맨 끝에서 true를 리턴해주기 때문에 동의어가 없는 경우나 동의어를 모두 추출한 이후 원본 AttributeSource도 후행 TokenStream에서 사용 할 수 있게 되어있습니다. 

이 개념이 조금 어려울실 수도 있겠지만 AttributeSource를 중심으로 생각하시면 조금은 쉽게 이해가 되실 것 입니다.
그리고 동의어 필터에서는 원본 Token으로부터 파생된 동의어들의 위치정보값을 모두 0으로 셋팅하여주고 있습니다.  

이것은 이전 Token(여기서는 원본 Token)과의 위치거리가 0이라는 뜻으로 결국 원본 Token과 같은 위치에 있는 동일한 Token이라는 의미로 사용됩니다. phraseQuery에서는 중요한 개념이지만 저희는 직접 QueryParser를 만들어 사용 할 것이기 때문에 크게 중요하게 사용되는 속성은 아닙니다. 

 하지만 꼭 알고 넘어가야하는 속성이기도 합니다.
PositionIncrementAttribute positionAttr = savedAttributeSource.addAttribute(PositionIncrementAttribute.class); //원본 AttributeSource의 Attribute를 받아옴 positionAttr.setPositionIncrement(0);
https://github.com/need4spd/aboutLucene 에서 체크아웃 받으 실 수 있습니다.


Posted by 용식
앞에 데모 프로그램을 보셔서 아시겠지만
검색이라는 하나의 작업을 위해서는 
크게 색인과 검색이라는 2가지 프로세스를 거쳐야 합니다.

이중 색인과 검색에 모두 관여하는 것이 하나 있는데
그것이 바로 키워드 추출.. 즉, 흔히 이야기하는(하지만 너무나 어려운)
형태소분석입니다. (이후에는 키워드 추출이라고 하도록 하겠습니다.)

잠깐 루씬을 떠나서
검색이론에 대해서 이야기 해 보겠습니다.

흔히 검색에서 이야기하는 색인 파일은 대부분 "역색인(Inverted Index)" 구조로 되어있습니다.

이게 어떤 구조인가하면.... 책의 맨 뒤 부록에 나오는
"단어"를 기준으로 페이지를 나열한 형태의 색인 구조라고 보시면 됩니다.

나이키 - Doc1, Doc2, Doc3
운동화 - Doc1, Doc5
청바지 - Doc6, Doc7


이런식으로 키워드를 기준으로 그 키워드가 포함된 문서들을
정의해 놓은 형태입니다.

"나이키"라는 검색어가 들어오면 Doc1, Doc2, Doc3을 찾아서 화면에
보여주는 것이지요.

"나이키와 운동화"라는 검색어가 들어오면
"나이키"에 연결된 Doc1, Doc2, Doc3
"운동화"에 연결된 Doc1, Doc5 를 찾아서
And 연산으로 Doc1을 찾아서 결과로 보여주는 구조입니다.

Doc1, Doc2, Doc3
&
Doc1, Doc5
= Doc1 


물론 재검색의 Boolean 조건이 OR라면 Doc1, Doc2, Doc3, Doc5가 전부 보여질 것 입니다.

보시면 아시겠지만 "키워드" 기반의 검색에 최적화 된 구조라고 볼 수 있습니다.

이러한 색인구조에는 미리 스코어를 지정 해 둘 수
없는 구조이기 때문에 Ranking이 그만큼 중요해지는 구조이기도 합니다.

그리고, 위의 예만 보시더라도
Doc1~Doc7까지의 문서에서 나이키, 운동화, 청바지등 사용자들에게
혹은 비지니스안에서 의미가 있는 키워드를 추출해내는 것과
사용자가 입력한 키워드인 "나이키와 운동화"에서 "나이키", "운동화"를 추출해내는 것이
얼마나 중요한 작업인가도 알 수 있습니다.

이 키워드가 추출되지 않으면
검색을 할 수가 없는 것 입니다.

이것이 검색에 있어서 키워드 추출(형태소 분석)이고
이 역할을 해주는 것이 루씬의 Analyzer입니다.

사실 루씬으로 검색엔진을 만든다고 할 때
가장 먼저 부딫히게 되는 장벽이 바로 이 Analyzer의 구현이고
또 가장 어려운 부분중에 하나가 역시 이 Analyzer입니다.

검색이라는 그 행동 자체는
어디서나 동일하겠지만 (랭킹의 구현은 다르겠지요..) 키워드 추출이라는 것은
나라별로, 언어별로 심지어 비지니스(1)별로 원하는 추출 방식이 모두 다르고, 추출해야하는
방법도 모두 다르기 때문입니다.

본격적으로 들어가기전에 키워드 추출에 대해서 조금 더 살펴보도록 하겠습니다.

초반에는 문장에서 (혹은 웹 페이지에서) 의미있는 키워드라는 것이 대부분 "명사"에 한정되는 구조였습니다.
전반적으로 색인 대상이 되는 문서가 많지 않았기 때문에 문장에 들어있는 중요한 명사들만
추출해서 색인을 해도 큰 문제가 없었울 것 입니다.

하지만 요즘에는 이렇게 명사만을 추출하는 것은 
큰 의미가 없을 것 같습니다.(2)

"사랑합니다"라는 노래를 찾아보기 위해 구글에서 "사랑"이라는 키워드로 검색을 해보세요.

"사랑"이 들어간 무수한 페이지들이 나올 것 입니다. 정말로 내가 원하는 것을 찾기 위해서는
"사랑"이라는 단어 하나만으로는 많이 부족합니다. 

최근에는 단순히 키워드만을 추출하는 것이 아닌, 점점 자연어에 근접한 키워드로 검색이 가능하도록
해주는 것이 중요해지고 있습니다. 물론 이러한 수준까지 이번 프로젝트(?)를 통해서
구현하지는 못 할 것 입니다. 사실 형태소 분석도 구현하기가 아주 어려워요..--;
참고로 이수명님께서 만드신 루씬용 형태소 분석기가 있습니다.

이번에는 미리 만들어진 사전을 기반으로 하여
문장과 입력된 키워드로부터 단어를 추출하는 수준으로
구현을 해 볼 예정입니다.

그리고 또 하나는 색인 할 때의 키워드 추출과 검색어로 들어온 키워드를 분석 할 때에는
약간의 차이가 있어야 한다는 것 입니다.

Doc1 : "삼성은 우리나라에서 가장 큰 기업이다. 전자부문을 비롯하여...." 
Doc2 : "삼성전자는 이번에 갤럭시S를 발표하였다."


라는 두개의 문장이 있다고 가정을 해보겠습니다.

Doc1에서의 키워드는 "삼성" , "우리나라" , "기업", "전자", "부문", "전자부문" 등이..
Doc2에서의 키워드는 "삼성" , "전자", "삼성전자", "갤럭시S", "발표" 등이 나온다고 볼 수 있을 것 입니다.

이렇게 색인이 되어 있는 상태에서
한명의 사용자가 검색어로 "삼성전자"를 입력하였습니다.

위 문장에서 키워드가 추출된 결과를 참고해보면 "삼성전자"로부터 "삼성"+"전자"
추출이 되는 것이라고 생각해 볼 수 있습니다.
그렇다면, 사용자가 "삼성전자"와 관련된 문서를 찾고 싶어서 입력 한 검색어를 "삼성""전자"
분석하여 검색이 되도록 해야 할까요? 그렇다면 Doc1과 Doc2과 모두 검색이 될 것 입니다.

하지만, Doc1은 사실 삼성전자와는 별 관련이 없는 글입니다.

오히려 사용자가 입력한 "삼성전자"라는 검색어를 특별히 분석하지 않고
그대로 검색이 되도록 한다면 Doc2만이 검색이 될 것이고 이것이 더 좋은
결과 값이라고 생각이 될 수도 있습니다.

사실 이러한 부분은 recall(재현율)과 precision(정확도) 중 어느 곳에
더 비중을 두느냐 혹은 위와 같은 상황에서 아예 Doc1을 검색 결과에서 제외를 시킬 것이냐
Ranking으로 해결 할 것이냐등에 따라서 정답이 없는 이야기이지만
키워드 추출이라는 것이 단어를 무조건 많이 추출한다는 것이
답은 아니라는 것을 보여드리기 위해서 언급을 해보았습니다.

이번에 직접 구현 할 Analyzer에서는 위 기준에 맞춰서
색인용과 검색어 분석용으로 나누어 구현을 할 예정입니다.

작성을 하고 이번 포스트를 마무리 하려고 하니..
이번 내용에서는 루씬에 대해서는 전혀 내용이 없네요..--;

하지만 검색 엔진을 만든다고하면 반드시 꼭 집고 넘어가야 하는
부분이기도 합니다. 최근의 검색 엔진들은 자연어 검색도 지원해주고 있기 때문에
여기서 이야기된 많은 부분들은 이미 검색엔진이 태동 될 초창기때
모두 한번씩은 언급이 되었던 내용들이라고 생각합니다.

하지만 뒤따라가는 입장에서
이미 지나간 이슈라고 그냥 넘어 갈 수는 없겠죠..^^

다음 포스트에서 
루씬에서 구현되어 있는 Analyzer의 구조와 가장 간단한 Analyzer하나를 뜯어보고
이를 바탕으로 저희만의 "사전기반의 명사 추출" Analyzer를 
만들어 나가보도록 하겠습니다.

하루써서 하루 올리는 입장이라서
글 올라오는 속도가 매우 들쑥날쑥 할 것 같습니다. 보통 애 재우고나야 
앉아서 뭘 쓸 시간이 생겨서요...ㅎㅎ

이해부탁드릴게요..^^

그리고 저도 검색/루씬 초짜입니다. 또 구현된 소스를
보시다보면 구현 자체에 문제가 있을 수도 있습니다.

내용 중 오류나 잘못된 부분들이 있으면
언제든 말씀해주세요.

감사합니다.

(1) - 쇼핑몰에서의 키워드 추출과 포털에서의 키워드 추출이 완전히 같을 수는 없을 것 입니다.
(2) - 쇼핑몰등에서는 조금 다릅니다. 명사/모델명등 단어만 추출하는 것이 충분히 의미가 있을 수 있습니다. 
(3) - 이 카페에 가시면 관련 내용들을 보실 수 있습니다.

Posted by 용식
루씬이란 무엇인가? 에서 부터 시작을 해봐야 할 것 같습니다.
루씬은 full text 검색엔진을 만들 수 있는
라이브러리를 제공합니다.

검색엔진이라고 하면
키워드분석 / 색인 / 검색의 과정을 수행하는 것으로
크게 나눠 볼 수 있겠는데, 이런 과정들을 실제로 구현 할 수 있는
API를 제공합니다.

우선 루씬을 사용해서
색인과 검색이 어떠한 방식으로 이루어지는지
예제를 보는 것이 가장 손쉽게 루씬에 대해서 알아 볼 수 있는 방법이 
될 것 같습니다.

이 예제들은 파일들로부터 내용을 색인하고
그 색인 파일을 사용하여 키워드로 내용을 검색하는 예제입니다.

굉장히 간단한 예제이지만
사실 검색엔진을 구현하는데 있어서 가장 필수적인
내용들은 거의 다 들어가 있다고 보셔도 됩니다.

그만큼 이 루씬이라는 라이브러리가 잘 만들어져 있다는
이야기도 되겠네요. 물론 스코어의 세부적인 컨트롤, 리플리케이션, 하이라이트등의
고급 기능은 포함되어 있지 않습니다.

사실 이번에 소개해드릴 데모 프로그램은 루씬 패키지를 다운 받으면 
보실 수 있는 데모 프로그램입니다. 
이 프로그램을 불필요한 내용들은  제거하여 다시 보여드리려고 합니다.

원 소스에 있던 주석들에도 좋은 이야기가 많기 때문에
원 주석은 그대로 두고
몇몇 부분의 주석을 더 하고 Line 단위로 
설명을 조금씩 붙여보려고 합니다.

간단하게 공유 할 용어들을 확인해보겠습니다.

색인과 검색은 다 아실거라 생각하고 색인의 대상이 되는 RawData..
이 RawData를 일반적으로 문서(Document)라고 부릅니다.
아마도, 검색이라는 것이 문서를 찾기 위해 발전된 기술이라서 그런 것이 아닐까 싶네요.

루씬에서도 색인을 하기 위해서 사용되는 클래스가 Document라는 이름으로
설계가 되어있습니다.

Document들이 색인되어 있는 것을 인덱스파일이라고 하며, 이 인덱스 파일에서
문서를 찾는 과정을 검색이라고 합니다. 그리고 검색된 Document들을 나열하는 것을
Ranking(혹은 Sort)... 이 Ranking에 사용되는 점수를 Score라고 합니다.

마찬가지로 루씬에서는 Sort, Score라는 이름이 사용되고 있습니다.


우선 색인 예제를 보시겠습니다.

색인 예제는 파일로부터 파일의 내용을 읽어서
이를 색인하는 예제입니다. 파일 그 자체를 Stream으로 읽어들이기 때문에
가급적이면 색인 대상 파일들이 txt파일등 일반 텍스트 문서로 되어 있으면 나중에
SearchFiles 데모에서 확인이 쉬울 것 입니다.

 
IndexFiles

내용 자체는 주석의 숫자를 순서대로 따라가시면 됩니다.
가장 기본적으로 색인 대상을 설정하고 색인 파일이 위치 할 곳을 지정합니다.

그리고 IndexWriter를 생성하여 
색인 대상으로부터 원문을 읽어 색인을 합니다.
이 과정에서 원문에서 키워드를 추출하기 원문분석을 해야하는데
이때 사용되는 것이 Analyzer입니다. 여기서는 StandardAnalyzer가 사용되었습니다.

각각의 Analyzer에 대해서는 나중에 자세히 살펴보려 합니다.

실제로 한국어에 맞는 형태소 분석을 하고 싶으면
Analyzer를 구현해야 합니다.

그리고 분석된 키워드를 바탕으로 색인을 하게 되는데 이때 사용 되는 것이
DocumentField 클래스입니다. 여기서는 하나의 파일이 하나의 Document가 됩니다.
필드는 원문내용과 최종수정일을 필드로 가져가게 되고요..

Document도 여러가지 설정이 존재하는데 이 부분 역시 IndexWrtier에 대한 이야기를
할 때 자세하게 작성해보도록 하겠습니다.

위 예제에서는 d:/의 파일들을 대상으로 확장자가 txt인 파일들만
색인을 하도록 되어있습니다.

그러면 검색은 어떻게 할까요?

 
SearchFiles

검색을 하기 위해서는 일단 색인 파일이 위치한 곳의 경로를 지정하고
이를 이용하여 IndexSearcher를 생성합니다. 그리고 검색어를 분석 할 Analyzer를 사용하여
검색어를 분석 후 루씬에서 사용 가능한 쿼리로 변환합니다.

이때 검색어 분석에 사용되는 것이 Analyzer이며, 쿼리를 만드는 것이 QueryParser
그렇게 나온 쿼리가 Query 클래스입니다.

각각에 대해서도 해야 할 이야기가 굉장히 많지만
우선 간단한 데모를 보는 것이 목적이기 때문에 자세한 이야기는 나중으로 
미루겠습니다.

위 예제에서는 색인된 파일을 대상으로
contents필드에 "eclipse"라는 키워드가 들어있는 파일을 검색하게 됩니다.

결과는 최종적으로 ScoreDoc이라는 타입으로 받아 올 수 있으며
ScoreDoc으로부터 Document를 받아 올 수도 있습니다.

검색 된 문서의 TF-IDF 점수를 볼 수도 있는 것을
예제를 통해 아실 수 있을 것 입니다.

이 예제가 루씬 색인/검색에 대한
핵심적인 예제입니다.

앞으로는 이 내용을 바탕으로 앞에서 언급된 중요 클래스들에 대해서
상세하게 알아보도록 하겠습니다.

앞으로 사용된 예제들은 
https://github.com/need4spd/aboutLucene을 통해서
공유하도록 하겠습니다. 위 예제들은 이미 올라가 있는 상태입니다.


수정
 - indexWriter의 updateDocument 메서드는 2.X 버전부터 있던 메서드입니다. 잘못된 내용을 알려주신 이문호님의 말씀에 의하면 "주어진 텀이 포함된 기존 문서들을 일괄 삭제한 후, 딱 하나의 새로운 문서를 추가하는 메서드입니다." 라고 하네요 ^^
그 이전 버전에서는 delete와 insert를 사용하여 직접 구현하는 방식을 사용해야 합니다. 

지적해주신 이문호님께 감사드립니다.
 


 


Posted by 용식
최근들어 루씬에 대해서
개인적으로 혹은 블로그등을 통해서
문의를 해주시는 분들이 많이 늘었습니다.

검색에 대한 관심이 커진 이유인지 잘 모르겠네요~
원래 재미있고 어려운 분야긴해서..

예전에 GS에 있을 때
루씬인액션 번역본 (1.4 기준) 책 한권을 들고
루씬으로 검색엔진을 만들어 본다고 한번 개발을 했던 (이게 벌써.. 3년 4년전이네요...)
경험과 그때 공부한 내용들을 이곳에 정리하긴 하였는데
사실 Case by Case로 작성한 포스트가 많아 처음 루씬을 접하시는 분들은
보시기가 쉽지 않으셨던 것 같습니다.

그 당시 나름 Solr에 대응한다고
Moon(ㅋㅋ)이라는 이름으로 프로젝트를 진행하였었고
한번 테스트 해볼만한 상태까지는 개발을 했었지만
외부 사정에 의해서 거기서 진행을 접을 수 밖에 없었죠.

지금 이곳 11번가에서도
개인적으로는 루씬을 사용해서 이것저것 해보고 있는 중이고
가능하면 루씬과의 연결된 끈을 놓지 않으려고 하고 있습니다.

대신 업무가 있기 때문에 깊게 파지는 못 하고 있어요..

아무튼 그때의 기억과 이곳에서의 경험을 이용하여
루씬으로 검색엔진 만들기라는 제목으로 포스트를 작성해보려고 합니다.

일단 루씬을 사용하여 기본적인 검색의 기능에
집중하려고 합니다.

검색 엔진을 만든다. 라고 하면 부딫히게 되는 몇가지 문제들이 있습니다.

1. 색인어 추출 (일명 형태소분석)
2. Ranking
3. 증분색인

등이 대표적인 문제인데요...

간단한 사전을 기반하여 사전에 등록된 키워드를 추출하는 Analyzer를 만들어 보는 것을
시작으로 이야기를 작성해보려고 합니다.

대략..
1. Analyzer
2. Indexing
3. Query 
4. Search
5. Score
6. Sort
7. MultiIndexing, MultiSearcher
8. Logging
9. HighLight
10. 그외 잡다한 지식


등이 될 것 같네요~

굉장히 긴 연재가 될 것 같기도 하고..
그당시 개발했던 소스를 지금봐도
당연히 가물가물하기도 하고.. 그때의 설계가 마음에 안들어서
다시 개발하여 이곳에 소개를 하기도 할 것 같습니다.

루씬 3.X버전을 사용하여
가능하면 필수적인 이야기들을
해보겠습니다.

다만, 컴퓨터공학을 전공하였지만
IR이 전공이 아닌지라 검색 그 자체에 대한
지식 기반은 저도 굉장히 얕습니다. 

그리고 루씬에 대해서
굉장히 뛰어난 실력을 가지신 분들도 사실 엄청 많으십니다. ^^
제가 아는 분들만 해도 루씬인액션 초판을 번역하신 강철구님이나 이문호
그리고 상당한 실력파로 보이시는 짐승님...
한글 형태소 분석기를 개발하신 이수명님..

이런 분들을 두고 글을 작성하는 것이 엄청 부담스럽기는 합니다. -_- 괜한 짓 하나 싶기도 하네요 ㅋㅋ

이 글은 오로지 루씬을 처음
사용하시는 분에 대한
가이드라인 정도를 제공해드린다고
이해해주시면 좋을 것 같습니다.


그 점은 양해를 부탁드리고..
 
음.. 이제 어떻게 쓸지 고민해봐야겠네요.. ^^;;; ㅋㅋ 
Posted by 용식