본문 바로가기

Lucene

[about Lucene] 루씬으로 검색엔진 개발하기 - Analyzer (5) -

이번에는 동의어 필터를 만들어보겠습니다. 동의어의 역할은 다 아시겠지만 특정한 키워드에 대해서 확장된 검색을 지원 할 수 있도록 해줍니다. 이 예제도 앞선 예제들과 마찬가지로 루씬인액션의 책에 있는 예제를 보고 우리 상황에 맞춰 다시 만든 코드들입니다. 먼저 테스트 케이스를 보겠습니다. 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 에서 체크아웃 받으 실 수 있습니다.