인덱싱을 할 때 사용되는 클래스인 Field도 4.0으로 넘어오면서

완전히 사용법이 변경되었습니다.


기존에는 아래와 같이 사용하였습니다.



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


4.0부터는 위처럼 Store, Index 등의 enum 타입을 파라메터로 갖는 생성자들은

모두 deprecated 처리가 되었고, 대신 FieldType이라는 클래스가 새로 생겼습니다.

위의 Field 인스턴스 생성 구문은 아래와 같이 대체가 가능합니다.



NumericField를 대신해서 각 number type별로 Field 클래스가 생겼습니다.



저작자 표시 비영리 변경 금지
신고
Posted by 용식

3.6 버전까지는 TokenStreamComponents를 재사용하기 위해서

커스텀 Analyzer를 만들때 ReusableAnalyzerBase 클래스를 상속받아야 했습니다.

그리고, reuseableTokenStream() 메서드를 실행하여 TokenStream을 얻어왔었는데요..


(참고 : http://devyongsik.tistory.com/377)


4.0에서 보니 ReusableAnalyzerBase 클래스가 없어지고 대신, TokenStreamComponents를 재사용하기 위해서

Analyzer.ReuseStrategy가 사용됩니다.


Strategy 패턴이고요.. 기본적으로 두개의 Strategy 구현 클래스가 제공됩니다.


1. GlobalReuseStrategy

2. PerFieldReuseStrategy


이름으로 쉽게 유추가 가능한데요...


Analyzer의 tokenStream(String fieldName, Reader reader) 메서드를 보시면 아래와 같이 구현되어있습니다.




components를 reuseStrategy.getReusableComponents 메서드를 사용해서 TokenStreamComponents를 가져오고 있습니다. 그게 없으면  createComponents 메서드를 사용하여 새로운 TokenStreamComponents를 만들어냅니다.

이 createComponents 는 Analyzer에 정의된 추상메서드로써, 커스텀 Analyzer를 만들때 구현해야하는 메서드입니다. WhitespaceAnalyzer를 보면 아래와 같이 구현되어있습니다.




템플릿메서드 패턴같이 보입니다...


아무튼, 여기서 말씀드리고 싶은 것은 reuseStrategy.getReusableComponents 인데요.. 기본적으로 2개의 구현클래스가 제공된다고 앞서 말씀을 드렸고, 별다른 설정을 하지 않으면 GlobalReuseStrategy가 셋팅됩니다. 


이 GlobalReuseStrategy.getReusableComponents  메서드를 보면 아래와 같이 되어있습니다.





여기서 getStoredValue는 ThreadLocal에 담겨져있던, TokenStreamComponents 입니다.

PerFieldReuseStrategy는 Map에 필드명으로 TokenStreamComponents를 저장합니다. 이 필드별로 저장하는 것은

어디에서 이점이 있는지 잘 모르겠네요.... 흠...


아무튼, 기본적으로 TokenStreamComponents를 사용하도록 되었네요. 4.0으로 버전업을 할 경우에 필수로 바꿔줘야 하는 부분인듯 합니다.


저작자 표시 비영리 변경 금지
신고
Posted by 용식

SearcherLifetimeManager입니다.


이 매니저클래스는 Searcher의 버전별 저장소라고 생각하시면 됩니다.

이 클래스가 없어도 Near Real Time Search를 사용 할 수 있는데요, 이 클래스가 필요한 이유를 아래의 블로그

http://www.searchworkings.org/blog/-/blogs/380754/maximized 에서는 이렇게 설명하고 있습니다.


무조건 새로운 버전의 IndexSearcher를 가지고, 실시간으로 인덱스의 변경 내용을 사용자에게 보여주는 것이

좋은 것은 아닙니다.


A라는 사람이 검색을 처음 시도하고, 1페이지에서 원하는 것을 찾지 못해 2페이지로 넘어가는 케이스에서, 다른 누군가에 의해 인덱스가 변경되어 새로운 IndexSearcher를 사용하여 페이징을 하게 되면 최악의 경우에는, A라는 사람은 똑같은 페이지를 다시 보게 되는 경우도 발생 할 수 있다는 것 입니다.


따라서, 페이징과 같은 연속된 검색 행동에 대해서는 처음 결과를 보여주었던 IndexSearcher를 그대로 사용하여 보여주는 것이 사용자에게 더 좋은 경험을 줄 수 있다는 것 입니다.


이럴때 사용하는것이 SearcherLifetimeManager입니다.


사용법은 간단합니다. 일단 IndexSearcher는 SearcherManager던 NRTManager던.. 다른 일반적인 방법으로 얻는다고 가정을 합니다.



SearcherLifetimeManager lifetimeManager = new SearcherLifetimeManager();

이 매니저 클래스는 몇가지 중요한 메서드를 제공합니다.


long recored(IndexSearcher searcher) 메서드는 현재 사용중인 searcher를 저장하고, 이 searcher의 버전 (여기서는 token이라고 부릅니다.)을 return 받습니다.


IndexSearcher(long token) 메서드는 위의 메서드로 저장된 token의 indexSearcher를 돌려받을 수 있습니다.


간단한 테스트케이스 소스입니다.


SearcherManager로부터 IndexSearcher를 받아냅니다. 처음에는 SearcherLifetimeManager에서 1 버전의 IndexSearcher를 요청하지만, record된 것이 없으므로 null이 return됩니다. 이후 record를 수행한 후 해당 버전의 IndexSearcher를 요청한 이후에는 정상적으로 검색을 수행 할 수 있습니다.

이렇게 return된 두개의 searcher는 아래의 조건을 만족합니다.



Assert.assertTrue(indexSearcher == newSearcher);


두번째 테스트케이스 코드입니다.




중간에 인덱스에 document를 하나 add하고 나서 새로 얻은 newSearcher를 record 한 후 이후 다시 lifetimeManager로부터 받아내어 검색을 수행하는 테스트 코드입니다. indexSearcher는 결과가 1, newSearcher는 결과가 2인 것을 확인 하실 수 있습니다.


또하나의 중요한 메서드가 있는데 바로 prune 메서드입니다.


void prune(Pruner p);


Pruner는 인터페이스인데, 이 인터페이스를 상속한 클래스에 의해서 record되어있던 indexSearcher들을 정리하는 역할을 합니다. 일단, 간단하게 이 인터페이스를 상속하여 구현한 클래스로 테스트한 코드입니다.



무조건 true를 return한게 되어있는데요, 그래서 record된 indexSearcher가 remove된 것을 보실 수 있습니다.

이 인터페이스를 구현한 기본 클래스가 하나 제공되는데요, PruneByAge 입니다.

생성자의 파라메터로 double 형을 받는데, 초단위로 생각하시면 됩니다.


테스트코드입니다.



이 메서드는 아래와 같이 설명이 되어있습니다.


/** Simple pruner that drops any searcher older by

   *  more than the specified seconds, than the newest

   *  searcher. */


가장 새로운 searcher와 비교해서 X초 이전의 searcher를 제거합니다. 위 테스트코드에서 firstSearcherToken으로 record된 indexSearcher만 삭제된 것을 보실 수 있습니다.


이 메서드는 새로운 searcher를 생성하는 스레드가 주기적으로 호출해주는 것이 좋다고 되어있습니다.


이 SearcherLifetimeManager를 활용하게 되면 굉장히 다양한 버전의 IndexSearcher가 생성되어 리소스에 큰 영향이 있지 않을까 우려가 있을 수도 있는데요, commiter의 설명으로는 리소스가 더 필요하긴 하겠지만 새롭게 생성되는 IndexSearcher들끼리는 점점 더 많은 sub-reader들을 공유 할 수 있기 때문에 우려 할 정도는 아닐 것이다 라고 이야기하네요.. 물론, 인덱스 파일의 굉장히 큰 변경 후 merge나 너무 잦은 index 파일의 변경으로 인한 reopen등의 조심해야 할 상황이 있기는 하겠습니다.


3.5부터 전반적으로 다루기 어려운 클래스들에 대해서 유틸성 클래스가 많이 제공되고 계속 변화되고 있는 것 같습니다.

사용하는 입장에서는 환영할 일이지만.. 개인적으로 이런 종류의 클래스를 필요에의해 만들면서 삽질하고 에러내고.. 하면서 배웠던 경험이 무척 소중했던지라... 앞으로 그런 기회마저 없어지는 것은 아닐지 좀 걱정이 되기도하네요. ^^


참고블로그 : http://www.searchworkings.org/blog/-/blogs/380754/maximized

테스트코드 : https://github.com/need4spd/aboutLucene



저작자 표시 비영리 변경 금지
신고
Posted by 용식

NearRealTime Manager입니다.


앞서 포스팅하였던 SearcherManager를 사용할때 생성자에 IndexWriter를 주면

IndexWriter로부터 NRT IndexReader (루씬사용자들이 아마.. IndexWriter로부터 얻어내는 IndexReader를 NRT IndexReader라고 하는 듯 합니다.)를 얻어내서, IndexWriter를 통한 Document add/update/delete등의 반영이후 commit이 없어도

mayRefresh() / acquire() 메서드를 통해 얻은 IndexSearcher가 변경된 내용을 가져 올 수 있는 예제를 봤습니다.


NRTManager 역시 Near Real Time Search를 위한 유틸성 클래스입니다.


다만 SearcherManager보다 좀 더 구체적인 API가 제공됩니다.


일단, NRTManager는 아래와 같이 생성합니다. 




생성자로 넘겨주는 IndexWriter가 그냥 IndexWriter가 아니라 NRTManager.TrackingIndexWriter인 것이 특징입니다.

기본적으로 IndexWriter가 가지고 있는 add/update/delete 등의 메서드를 가지고 있고, commit/close는 가지고 있지 않습니다. 이 TrackingIndexWriter의 특징은 위의 add/update/delete 메서드를 통해 인덱스에 변경이 일어날 경우 Generation Token 값을 넘겨준다는 것 입니다.


예를들어서 위의 처음 NRTManager를 생성한 경우에는 generation이 0이고, TrackingIndexWriter를 통해서 Document를 하나 add 할 때 return되는 generation의 값은 1입니다. 이 generation이 NRTManager하고도 연결이 되는데요.. 즉, generation이 0인 IndexSearcher는 새로 add된 Document를 반영하지 못 하고, 1인 IndexSearcher는 이를 반영 할 수 있게 됩니다.


물론 NRTManager에서는 이 generation의 값으로 해당 generation에 속하는 IndexSearcher를 얻을 수는 없지만, 이를 위한

클래스가 또 따로 준비가 되어있습니다. (SearchLifeTimeManager) 이는 나중에 따로 또 포스팅 할 예정입니다.


아래의 테스트 코드를 보겠습니다.



처음 nrtManager로부터 가져온 generation은 0이고, Document를 add하고 maybeRefresh를 실행 한 다음의 generation 값은 1입니다. 잘 보셔야 할 부분은, trackingIndexWriter.addDocument를 실행하면서 return된 newGeneration의 값이 1이라는 점입니다. 이를 통해서 maybeRefresh를 실행 한 이후의 Generation 값을 예측 할 수 있습니다.

참고로, addDocument를 연속으로 두번한다고해서 newGeneration의 값이 계속 올라가는 것은 아닙니다. 어디까지나 currentGeneration를 기준으로 생성됩니다.


또한, nrtManager에는 waitForGeneration이라는 메서드가 있습니다.

아래의 테스트 코드를 보겠습니다.



일단, watiForGeneration 메서드 자체는 지정된 generation이 currentGeneration이 될 때까지 기다립니다.

위 첫번째 테스트 코드의 경우 , nrtManager.waitForGeneration(1); 은 아무런 역할을 하지 않습니다. maybeRefresh() 메서드가 먼저 실행되면서 currentGeneration이 1이 된 상태이기 때문입니다. 만약, nrtManager.waitForGeneration(2); 였다면

다른 어딘가에서 maybeRefresh()를 실행하여 currentGeneration을 증가시키 않는이상 계속 blocking상태로 있게 됩니다.


nrtManager.waitForGeneration(3); 이면 어떻게 될까요?


Exception이 발생합니다.


그리고, WaitListener를 구현하면, waitForGeneration실행시의 추가 동작을 정의 할 수도 있습니다.


이제, 실제 Near Real Time Search의 예제 코드를 보겠습니다.



코드를 보시면 indexSearcher가 각각 다른 결과를 보여주는 것을 보실 수 있습니다.


아래의 블로그에서는 NRTManager에서 이 generation 값을 가지고 SearcherManager를 얻어서 해당 generation의 IndexSearcher를 얻어 올 수 있다고 되어있는듯 한데.. 3.6.0으로 넘어오면서 그 기능이 사라지고 대신, SearchLifetimeManager가 새로 생긴 듯 합니다. 


그래서인지... 이 NRTManager만 가지고는 generation token으로 할 수 있는 것이 좀 애매합니다. 3.6에서는 해당 generation의 Searcher나 SearcherManager를 가져 올 수가 없어서....


왠지 4.0에서는 또 완전히 다 바뀌어 있을 것 같은데.... 좀 봐야겠네요...


참고블로그 : http://blog.mikemccandless.com/2011/11/near-real-time-readers-with-lucenes.html

테스트코드 : https://github.com/need4spd/aboutLucene



저작자 표시 비영리 변경 금지
신고
Posted by 용식

루씬 3.5버전부터 제공되는 유틸리티성 클래스입니다.

IndexSearcher의 재사용과 IndexWriter로 변경된 인덱스를 새로 가져오기 위해

저런 매니저 클래스를 매번 새로 만들어서 사용했었는데 아예 3.5 버전부터 제공이 되어 나오네요.

(Lucene in Action 2판에도 소개가 되어있습니다. 361P)


여러 스레드가 IndexSearcher 인스턴스를 안전하게 열고 닫고 하면서 사용 할 수 있는

기능을 제공하여 줍니다. acquire/release API를 제공하며, IndexReader를 열고 닫기 위한 병렬처리의 복잡함을

알아서 처리해주도록 구현이 되어있습니다.


3.5버전과 3.6버전에서 SearcherManager 인스턴스를 생성하는 방법이 조금 바뀌었는데요.. 

(생성자의 파라메터가 변경되었습니다. 몇몇 메서드 이름하고..)


일단 3.5버전에서는 아래와 같이 생성합니다.


3.5 버전

3.6에서는 아래와 같이 SearcherFactory를 사용합니다. 이 클래스는 특별히 하는 것 없고 IndexReader를 받아서 새로운 IndexSearcher를 생성해 넘겨주는 역할을 합니다.


3.6버전

IndexWriter를 SearcherManager의 생성자의 파라메터로 넘겨주는 방법도 있습니다. 이 경우에는 Near Real Time IndexReader를 사용하기 때문에 mayberefresh 메서드를 실행함으로써 commit 없이도 새로 인덱스에 반영 된 내용을 가져 올 수 있습니다.

indexSearcher를 얻고 반환하는 방법은 아래와 같이 합니다.



몇 가지 테스트 케이스를 작성해서 돌려보았습니다.

1. indexWriter로 addDocument를 한 후 commit 없이 acquire

2. indexWriter로 addDocument를 한 후 commit 하고 acquire

3. indexWriter로 addDocument를 한 후 commit 없이 , maybeRefresh 메서드 실행 후 acquire

4. indexWriter로 addDocument를 한 후 commit 하고 , maybeRefresh 메서드 실행 후 acquire

5. SearcherManager를 IndexWriter로 생성하고 , 이 IndexWriter로 addDocument를 한 후 commit 없이 acquire

6. SearcherManager를 IndexWriter로 생성하고 , 이 IndexWriter로 addDocument를 한 후 commit 없이 maybeRefresh

메서드 실행 후 acquire


인덱스 파일의 변경내용을 indexSearcher가 어떤 경우에 반영하는지에 대한 테스트인데요

결론을 먼저 말씀드리면 4번, 6번의 경우에만 반영합니다. 이외에도 Near Real Time Search를 위한 NRTManager가 제공되는데 이 부분은 다음 포스트에 작성을 하고... 테스트 케이스를 보여드립니다. 

사실 6번의 케이스가 NRT Search랑 비슷하겠네요..




좀 더 자세한 내용은 아래의 블로그 (루씬 커미터이자 Lucene in Action의 저자)를 참고하여 주세요.


http://blog.mikemccandless.com/2011/09/lucenes-searchermanager-simplifies.html


테스트 소스 : https://github.com/need4spd/aboutLucene




저작자 표시 비영리 변경 금지
신고
Posted by 용식

파이썬 스터디가 반쪽 성공(반쪽 실패?)으로 끝난 이후

다음 스터디를 준비하는 과정에서..


얼마전 시리즈로 포스팅하였던

about Lucene에서 기본적이고 쉬운(?) 내용들과

실제 사내에서 활용한 사례를 가지고 루씬에 대한 사내 세미나를 2회에 걸쳐 진행하려고 하고 있습니다.

(너무 길면 힘들다.. 라는 교훈도 지난 스터디를 통해 얻었습니다. -_-)


문서를 만들고 있는데...

https://docs.google.com/open?id=0B9vUAZZ3ZLlYYnpCeHN0bWNRVi00ZmlDRFI5N1Ywdw

멋들어진 문서 만들기라는 것이 정말 쉽지 않네요.


검색에 대해서 모르시는 분들

루씬에 대해서 모르시는 분들을 대상으로 하는

세미나이기 때문에 내용이 몹시 평이하고, 좀 세부적인 내용들은

다 제외된 상태이기는 하지만 이렇게 또 루씬의 사용자가 늘어날 수 있다면

나름 좋은일(?) 아닐까요 ㅎㅎ


저작자 표시 비영리 변경 금지
신고
Posted by 용식

검색결과에 대한 기본적인 정렬은
TF-IDF 의한 유사도 점수입니다. 하지만 검색결과를
우리의 입맛대로 조절해야 하는 경우가 있습니다. 이를 위해서 루씬에서는
Sort클래스와 그외 몇가지 확장 가능한 API 제공합니다.

여기서는 우선 Sort클래스의 기본적인 사용법을 확인해보겠습니다.

 

SortTest.java

보시면 2 필드에 대해서 정렬을 하고 있습니다.
Sort클래스의 사용은 우선 정렬에 사용 SortField 생성하고 이를 사용해
Sort클래스의 인스턴스를 만들어서 IndexSearcher 통해 검색을 합니다.

여태까지
IndexSearcher로부터 받아오는 결과 클래스가 TopDocs였지만
필드 정렬을 사용 때는 TopFieldDocs라는 클래스로 결과가 받아집니다.
외에도 Collector 사용하여 결과를 받아 수도 있습니다.

Field 의한 정렬이 되기 위해서는 해당 필드가
NOT_ANALYZED 색인이 되어 있어야 합니다. 그래서 예제 같은 경우
검색을 위한 title 외에 title2라는 필드를 추가로 생성하고 정렬에 사용하고 있습니다.

하나는 SortField 설정하는 SortType 색인시 Field 설정한 타입이 같아야 합니다.

만약 예제에서 sortByField 메서드의 SortField 타입을 INT형으로 바꾼다면
예제는 에러가 발생합니다.

위의 예제가 기본적으로 루씬에서 제공되는 정렬 방법입니다.
다만 정렬이라는 것이 전체 데이터셋을 구한 상태에서의 작업이기 때문에 결과셋이 많을 수록
(즉, 정렬해야 하는 대상 문서가 많을수록...) 속도나 리소스 사용 측면에서 성능이 떨어지는 경우가
있습니다.

이를 조금 다르게 생각해보면
전체 결과를 구해놓고 정렬하는 것이 아니라 TF-IDF점수를 활용하여 정렬하는 것 처럼
대상 문서를 가져 올 때 정렬을 시키면 어떨까? 라는 생각을 해 볼 수 있습니다. 

"짐승"이란 닉네임으로 활동하고 계시는 진성주님의 블로그를 참고해보세요.
(http://blog.softwaregeeks.org/archives/89)

보시면 CustomScoreQuery 클래스를 활용하여 위에서 얘기되었던 고민을 해결하고 있습니다.
즉, 검색 대상 문서를 찾으면서 정렬하는 방식이지요...

이 부분에 대한 설명은 예전에 제가 CustomScoreQuery 사용하여 정렬에 대한 예제와 설명을
작성했던 글이 있습니다.

이 부분은 그 포스트들의 링크로 대신하려고 합니다.

루씬에서뿐만이 아니라 어느 검색엔진에서도 결과Set굉장히 클 때의 정렬은 고민거리가 됩니다.
여러분께서도 루씬을 사용하시면서 문서의 수가 점점 커질 수록 이 부분에 대한 고민이
되실 것 입니다. 그때 하나의 해결책으로써의 방법이 될 수도 있으니 꼭 한번 읽어보시기를 권해드립니다.


이 포스트까지 하면 루씬을 활용하여 검색엔진을 만들어보기 위한
기본적인 내용들은 다 작성을 한 것 같습니다

사실 Analyzer의 구현이 가장 어려운 부분이고 끊임없이 손을대고 사전을 다듬어야 하기 때문에
가장 번거로운 부분이기도 합니다. Analyzer를 제외한 나머지 부분들은 잘 만들어진 API를 잘 사용하는 것이기 때문에
직접 사용하시면서 숙달하시는 방법이 가장 좋은 방법이구요..

이후에는 Replication에 대한 내용과
그외 생각나는 Tip과 경험담의 위주로 내용을 작성해나가려고 합니다.

실제로 하나의 완성된 검색엔진 개발 과정을 보여드리려고 했는데
내년 정도에 한번 진행을 해보게 될 것 같네요.

about Lucene 카테고리 외에 Lucene 카테고리에도 이미 경험했던 내용들에 대한
많은 내용들이 있으니 참고해주세요.
 
예제는
https://github.com/need4spd/aboutLucene
에서 받으 실 수 있습니다. 

CustomScoreQuery에 대한 예제 소스는
패키지 com.tistory.devyongsik.customscore 밑에 있습니다. 

저작자 표시 비영리 변경 금지
신고
Posted by 용식
이번 포스트에서는 루씬에서 사용되는 QueryFilter의 기본적은 사용예제를
보여드리려고 합니다.

다시 예전에 작성했던 IndexSearcherTest의 메서드들을 살펴보면
4가지 쿼리 타입에 대해서 테스트 케이스가 작성되어 있는 것을 보실 수 있습니다.

  IndexSearcherTest.java

각 메서드를 간단하게 살펴보면 우선 searchByTerm 메서드에서는
Term을 사용하여 Query를 만들고 있습니다.

Term t = new Term("ids", "1");
Query q = new TermQuery(t);


위 쿼리는 ids 필드에서 값이 1인 Document를 검색하고 싶다는 뜻입니다.

searchByBooleanQuery 메서드에서는 Term을 사용한 Query 2개를 BooleanQuery를 사용하여
연결하고 있습니다.
 

BooleanQuery resultQuery = new BooleanQuery();
Term t = new Term("ids", "1");
Query q = new TermQuery(t);
resultQuery.add(q, Occur.SHOULD);
Term t2 = new Term("contents", "ibatis");
Query q2 = new TermQuery(t2);
resultQuery.add(q2, Occur.SHOULD);



위 예제는 ids가 1 이거나 contentsibatis 키워드가 들어가 있는 Document를 찾겠다는 뜻 입니다.
두개의 쿼리를 연결 할 때 Occur.SHOULD가 사용되는데 이것은 두 쿼리를 OR로 연결하겠다는 뜻 입니다.
이외에 MUST (and)MUST NOT(and not) 등이 있습니다.

searchByTermRangeQuery 메서드에서는 TermRangeQuery를 사용한 예입니다.
titles2h ~ j 사이 (첫 알파벳 기준)에 있는 Document를 검색합니다. 이때 대상 filedIndexing은 되어야 하고
Analyze는 되어있지 않아야 합니다.

Query q = new TermRangeQuery("titles2", "h", "j", true, true);
TopDocs docs = indexSearcher.search(q, 10);



searchByNumericRangeQuery 메서드는 NumericRangeQuery를 사용한 예 입니다.

Query q = NumericRangeQuery.newIntRange("price", 2000, 4000, true, true);



여기서는 price2000 ~ 4000 사이의 Document를 검색합니다. 맨 끝 boolean 파라메터 2개는 2000과 4000을 포함 할 것인지여부입니다. true가 포함하는 것 입니다.

NumericRangeQuery 쿼리의 static 메서드는 newIntRange말고도 LongRange등 여러 종류의 메서드가 제공되는데
한가지 주의 하실 점이 이 쿼리를 사용하시려면 색인시에 해당 필드의 타입을 맞춰주어야 합니다.
위 예제로 보면 IntRange를 사용하고 있기 때문에 색인시에 위 필드는 아래와 같이 색인 되어야 합니다.

NumericField numField = new NumericField("price", Field.Store.YES, true);
numField.setIntValue(prices[i]);



물론, TermRangeQuery처럼 Indexing은 되어 있어야하고, Analyze는 되어있지 않아야 합니다. (숫자이니 딱히 Analyze 할 건 없겠죠..)

추가적으로 Term쿼리와 Boolean쿼리에 대해서 말씀을 드리려고 합니다.

색인시 whiteanalyzer에 의해서 "lucene in action"의 문장이 [lucene]+[in]+[action]으로 분석되어
색인이 되었는데 검색시 Term t = new Term("title", "lucene in action"); 과 같은 형식으로
쿼리가 만들어지게 되면 절대로 검색이 되지 않을 것 입니다.

일반적으로 사용자가 입력 한 키워드를 스페이스로 구분하여 두개의 TermQuery를 생성하고
이를 BooleanQueryAND 조건으로 조합하면 키워드에 대해서는 괜찮은 쿼리식을 만들 수 
있습니다. 다만, 띄어쓰기를 하지 않는 사용자도 있기 때문에 이 부분에 대해서는
Analyzer를 활용하여 쿼리를 만들어 볼 수도 있습니다.

BuildQueryTest.java

보시면 사용자가 입력 한 키워드에 대해서 Analyzer로 분석을 하여 Boolean 쿼리를 
생성하고 있습니다. 이런 방식으로 키워드를 추출하여 검색 쿼리를 만드는 방법도 있으니
참고하시면 좋을 것 같습니다.

검색쿼리에서 Boolean쿼리를 사용하여 원하는 결과 값만을 가져 올 수 있지만
루씬에서는 Filter 클래스를 사용하여 마찬가지로 원하는 결과를 추려낼 수 있습니다.

FilterTest.java

메서드가 2개가 있는데요 하나는 Term에 의한 필터링
하나는 Range를 사용한 필터링입니다. Boolean 쿼리에 의한 필터링과 가장 큰 차이점은
필터는 점수에 대한 관여를 하지 않는다는 것 입니다.

관련 내용은
http://stackoverflow.com/questions/3721020/lucene-what-is-the-difference-between-query-and-filter
여기를 보시면 아실 수 있을 것 입니다.

필터를 사용 할 때 주의 할 부분이 있는데요
Term에 의한 필터링을 할 때는 해당 필드의 데이터가 NOT_ANALYZED로
되어 있어야 값이 정상적으로 나옵니다.

그리고 Range에 의한 필터는
해당 필드가 같은 NumericType으로 색인이 되어있어야
정상적으로 결과를 가져 올 수 있습니다.

RangeFilter에서 맨 끝의 boolean 파라메터 두개는
경계값을 가져 올 것인지 말 것인지에 대한 boolean 값입니다. true가 값을
포함하여 가져오도록 되어있습니다.

책을 보시면 CustomFilter를 직접 개발하여 사용하는 예제도 있으니
관심이 있으시다면 꼭 한번 보시기를 권해드립니다.

쿼리와 필터를 적절히 사용하시면 검색 식을 구현하는데 큰 무리가 없으실 것이구요..
검색에 있어서 가장 중요한 것은 아무튼 색인시에 얼마나 키워드가 잘 추출되었는가와
사용자가 입력한 키워드로부터 색인시 추출 되었던 키워드를 얼마나 잘 추출하여 쿼리식을
구성하는지 입니다.

보시면 아시겠지만 API 자체가 워낙 잘 구성이 되어있기 때문에
실제 사용에 있어서 큰 어려움은 없으실 것이라 생각됩니다.

예제는
https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다. 


저작자 표시 비영리 변경 금지
신고
Posted by 용식
이번 글에서는
루씬인액션에서 소개되고 있는
Near Realtime Search에 대해서 작성해보려고 합니다.

기본적으로 IndexSearcherIndexWriter에 의한 변경 사항을
바로바로 반영하지를 못 합니다. 일반적으로 commit이 된 이후 IndexSearcher를 새로 생성하여야
IndexWriter에 의한 변경된 내용을 반영 할 수 있습니다.

제가 내부적으로 구현하여 사용하고 있는
프로그램에서도 이러한 부분을 Searcher를 새로 만들어서 사용하고 있습니다.

  RealTimeSearch.java

파일명은 RealTimeSearch이지만 사실 RealTimeSearch에 대한 예제는 아닙니다.

지금까지 루씬에서 IndexWriter에 의한 변경을 IndexSearcher에서 반영하기 위해 사용했던
방식을 테스트케이스 형태로 구현해둔 것 입니다.

우선 init()메서드를 통해 메모리에 문서를 색인하여 놓습니다.
그리고 private으로 deleteDocument, addDocument 메서드를 구현하였습니다.
이를 사용하여 IndexWriter에 의한 변경 내용이 어떻게 반영이 되는지 확인해 볼 것 입니다.

첫번째 테스트케이스 searchAfterDocumentDeleted 에서는
ids가 1인 문서를 찾고...  deleteDocument 메서드를 실행하여 ids가 1인 문서를 삭제합니다.
그리고 다시 ids가 1인 문서를 찾고 있습니다. deleteDocument에서 commit까지 하였지만 이미 생성되어 있던
IndexSearcher는 이 내용을 반영하지 못 합니다.

searchAfterDocumentDeleteIndexReaderReopen 메서드를 보시면
마찬가지로 ids가 1인 문서를 삭제를 한 후 ids가 1인 문서를 검색하는데 그 전에 IndexReader를 다시 생성하는
부분이 있습니다.

IndexReader newReader = indexReader.reopen();
if(newReader != indexReader) {
indexSearcher = new IndexSearcher(newReader);
indexReader.close();
}


바로 이 부분이 지금까지 사용했던 IndexWriter에 의한 변경 내용을 반영하는 방법이었습니다.
이렇게 구현을 해도 기능은 이상없이 잘 적용이 됩니다. 하지만 책에서는 IndexWritercommit 작업이 디스크 IO를 유발하고
기타 부하를 주는 작업이라고 하네요.. 그래서 commit을 하지 않아도 IndexSearcher가 변경된 내용을 반영 할 수 있는
NearRealTime Search가 바로 이번에 루씬인액션에서 소개되었습니다.


NearRealTimeTest.java
코드가 좀 길지만 맨 위 메서드가 책에서 나온 예제이고..
그 아래 두개의 메서드는 첫번째 메서드에서 deprecated된 메서드의 사용을 제가 수정한 메서드 두개입니다.

내용의 핵심은 IndexWrtierIndexReader를 얻어서 이를 사용하여 IndexSearcher를 생성하는 것 입니다.
기본적으로 IndexReader를 다시 open하는 작업은 동일하지만 IndexWritercommit을 하지 않아도
변경 된 내용을 IndexSearcher가 반영 할 수 있다는 부분이 다른 점 입니다.

IndexSearcher는 아래와 같이 생성합니다. 

IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED);
IndexReader reader = writer.getReader();
IndexSearcher searcher = new IndexSearcher(reader);


다만, 현재 3.3 버전에서는 IndexWriter.getReader 메서드가 deprecated처리가 되었기 때문에
이 부분을 아래와 같이 수정하였습니다.

IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_33, new WhitespaceAnalyzer(Version.LUCENE_33));
IndexWriter writer = new IndexWriter(dir, conf);
IndexReader reader = IndexReader.open(writer, true);
IndexSearcher searcher = new IndexSearcher(reader);


테스트케이스를 보시면 commit을 하지 않아도 문서가 추가되고 삭제된 내용이 IndexSearcher에 의해서
반영되고 있는 것을 확인 하실 수 있습니다.

https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다. 


저작자 표시 비영리 변경 금지
신고
Posted by 용식
앞선 포스트에서 IndexWriter를 사용한 색인을 알아보았습니다.
이번에는 IndexSearcher를 사용한 Document 검색과 검색을 할 때 사용되는
Query에 대해서 알아보고 더불어..Filter까지도 함께 알아보도록 하겠습니다.

우선 간단한 IndexSearcher의 테스트 케이스입니다.
 
 
IndexSearcherTest.java

특별히 어려운 코드는 없을 것 입니다.
테스트 케이스가 4가지가 있는데요 나중에 이야기 할
Query 종류에 따른 사용을 보여드리기 위해서 각각 테스트 케이스를 작성하였습니다.

IndexSearcher를 사용 할 때의 주의 할 점은 여러가지가 있겠지만
그 중 가장 중요한 것이 IndexSearcher의 생성은 비용이 많이 들어가는 부분이기 때문에
가급적 단일 인스턴스를 여러 스레드가 사용 하는 형식으로 구현을 하는 것이 좋고
IndexWriter에 의한 변경 내용을 바로 IndexSearcher가 확인 할 수 없기 때문에 이에 대한 처리를
고민해봐야 합니다. (이 부분은 나중에.. 다시 언급 할 예정입니다.) 

IndexSearcher는 기본적으로 Query 클래스와 검색을 해 올 Document의 숫자를
지정하도록 되어있습니다.

TopDocs docs = indexSearcher.search(Query, 10);


Query는 나중에 따로  이야기를 할 예정이고 일반적으로 최초 검색 결과 페이지에서
보여 줄 정도의 개수만을 검색하고 이후 페이징에 따라서 검색 해올 개수를 늘려가는
방식으로 구현합니다. 

 이렇게 받아온 TopDocs 클래스로부터 ScoreDoc[] 을 얻어냅니다.

ScoreDoc[] hits = docs.scoreDocs;

//이 ScoreDoc은 실제 검색 된 Document의 내부 ID와 점수를 가지고 있습니다.

for(int i = 0; i < hits.length; i++) {
               System.out.println(hits[i].doc);
               System.out.println(hits[i].score);
          }


위 주석에도 작성하였지만 이 ScoreDoc은 자기 자신의 docID와 (색인 파일내에서 유일한 ID 값입니다.) 점수를 가지고 있습니다.

그리고 검색 된 내용을 가지고 오고 싶을 때는 아래와 같이 구현합니다. 

 Document resultDoc = indexSearcher.doc(hits[i].doc);
System.out.println(resultDoc.get("titles"));


실제 Document로부터 색인시 필드의 옵션 중 Store 옵션을 YES 설정 한 필드에 대해서는
String으로 된 필드의 내용을 얻어 올 수 있습니다.

앞에서 잠깐 언급하였지만 IndexSearcherIndexWriter에 의한 변경 내용을 자동으로 반영하여 보여주지 못 합니다.
이 부분에 대한 처리를 같이 살펴보고.. 2.9부터 지원이 되는걸로 생각이 되는 Near Real Time Search에 대해서
살펴보고 그 후에 쿼리들에 대한 이야기를 해보려고 합니다.

예제 소스들은https://github.com/need4spd/aboutLucene
에서 체크아웃 받으 실 수 있습니다. 


저작자 표시 비영리 변경 금지
신고
Posted by 용식