Today I Learned

[루씬 인 액션] 8장. 필수 확장 기능 본문

루씬 Lucene

[루씬 인 액션] 8장. 필수 확장 기능

하이라이터 2021. 2. 9. 02:36
728x90

8.2 분석기와 토큰 필터

contrib 모듈에는 상당 수의 언어별 분석기가 들어있으며, 여러 종류의 필터와 토큰 추출기가 제공된다.

8.2.1 스노볼 분석기

SnowballAnaylzer 클래스는 여러 언어의 기본형을 찾아주는 스테머(stemmer)의 기초가 된다.

스노볼 분석기를 통해 다양한 언어의 기본형 찾기 기능을 구현할 수 있다.

public void testEnglish() throws Exception {
    Analyzer analyzer = new SnowballAnalyzer(Version.LUCENE_30, "English");
    AnalyzerUtils.assertAnalyzesTo(analyzer,
                                   "stemming algorithms",
                                   new String[] {"stem", "algorithm"});
  }

 

8.2.2 N그램 필터

N그램 필터는 토큰을 글자 단위의 N그램 토큰으로 생성하며, N그램 토큰은 연결된 글자의 모든 조합의 의미한다.

 

예제 8.1 N그램 필터로 인접 문자의 결합

public class NGramTest extends TestCase {

  private static class NGramAnalyzer extends Analyzer {
    public TokenStream tokenStream(String fieldName, Reader reader) {
      return new NGramTokenFilter(new KeywordTokenizer(reader), 2, 4);
    }
  }

  private static class FrontEdgeNGramAnalyzer extends Analyzer {
    public TokenStream tokenStream(String fieldName, Reader reader) {
      return new EdgeNGramTokenFilter(new KeywordTokenizer(reader), EdgeNGramTokenFilter.Side.FRONT, 1, 4);
    }
  }

  private static class BackEdgeNGramAnalyzer extends Analyzer {
    public TokenStream tokenStream(String fieldName, Reader reader) {
      return new EdgeNGramTokenFilter(new KeywordTokenizer(reader), EdgeNGramTokenFilter.Side.BACK, 1, 4);
    }
  }

  public void testNGramTokenFilter24() throws IOException {
    AnalyzerUtils.displayTokensWithPositions(new NGramAnalyzer(), "lettuce");
  }

  public void testEdgeNGramTokenFilterFront() throws IOException {
    AnalyzerUtils.displayTokensWithPositions(new FrontEdgeNGramAnalyzer(), "lettuce");
  }

  public void testEdgeNGramTokenFilterBack() throws IOException {
    AnalyzerUtils.displayTokensWithPositions(new BackEdgeNGramAnalyzer(), "lettuce");
  }
}

 

testNgramTokenFilter24 결과

testEdgeNGramTokenFilterFront 결과

testEdgeNGramTokenFilterBack 결과

8.2.3 싱글 필터

싱글(single)은 여러 개의 연결된 토큰을 하나로 묶은 토큰을 의미한다.

글자 단위로 동작하는 N그램과 달리, 싱글은 단어 단위로 동작한다.

 

싱글 토큰을 사용하면 널리 사용되는 단어를 포함하는 구문의 검색 성능을 높일 수 있다.

싱글 토큰을 불용어를 제거하지않아 불용어를 포함하는 구문 질의로 처리 가능하다.

ex) 'Wizard of Oz' 구문을 'Wizard', 'of', 'Oz'로 자르는 대신, 'Wizard of', 'of Oz'로 토큰을 생성해 성능을 향상

 

내용이 비슷한 문서끼리 묶어주는 문사 자동 분류 작업 시에도 유용하다.

문서 안에서 가장 눈에 띄는 싱글 토큰을 파악해 싱글토큰의 빈도수로 문서를 분류 할 수 있다.


8.3 검색 질의 하이라이팅

contrib 모듈의 하이라이팅 기능은 원문 텍스트에서 질의에 해당하는 부분을 골라내 사용자에게 보여준다.

또한 원문에서 질의의 주변 부분만 읽어봐도 사용자가 찾으려던 정보인지 명확하게 판단할 수 있는 경우가 많다.

 

하이라이팅은 두 개의 서로 다른 기능으로 구성된다.

1. 동적 문단 추출(dynamic fragmenting) : 동적으로 원문에서 질의와 관련된 요약문을 추출

2. 하이라이팅 : 검색 질의의 단어를 굵은체나 글자색 등으로 처리

 

8.3.1 하이라이팅 모듈

TokenSources

하이라이팅 작업 시에는 원문(String)과 원문을 기반으로하는 토큰스트림(TokenStream)이 필요하다.

색인할 때 사용했던 분석기로 원문을 다시 분석하거나, 색인 시에 Field.TermVector.WITH_POSITIONS_OFFSETS 필드 설정하여 색인에 저장된 텀벡터를 기반으로 TokenStream을 사용할 수 있다.

 

하이라이터는 토큰 스트림에서 받아온 각 토큰의 시작과 끝 지점에 의존해 원문에서 하이라이팅해야 할 글자의 위치를 찾아낸다.

따라서 각 토큰의 startOffset과 endOffset 속성 값을 정확하게 지정해야 올바른 결과를 얻을 수 있다.

 

Fragmenter

하이라이터 패키지 안에 들어있는 자바 인터페이스이며, 원문 텍스트를 개별 조각으로 분리하는 기능을 담당한다.

NullFragmenter : 전체 원문을 하나의 조각으로 간주하여 분리

SimpleFragmenter : 글자 수에 맞춰 고정된 크기의 조각으로 분리

SimpleSpanFragmenter : 질의에 해당하는 스팬을 정확히 포함하게 분리

 

Scorer

Fragmenter에서 출력된 텍스트 조각 중 가장 적당한 조각을 선택하는 기능을 담당한다.

QueryTermScorer : 지정된 Query 객체의 텀 중 해당 조각 안에 포함된 텀의 개수를 사용해 점수를 부여

QueryScorer : 해당 문서에서 실제로 점수에 영향을 준 텀에만 점수를 부여

 

Encoder

원문의 텍스트를 화면에 출력할 형식으로 변환하는 기능을 담당한다.

DefaultEncoder : 텍스트를 변환하지 않는 기본 인코더

sipleHTMLEncoder : 문서를 HTML 표준에 맞는 형식으로 변환

 

Formatter

각 조각의 텍스트와 하이라이팅할 텀을 넘겨받아 조각의 텍스트 중 텀에 해당하는 부분을 하이라이팅하는 기능

 

8.3.2 단독 실행 예제

최적의 원문 조각을 찾아낸 후 해당하는 텀마다 <b> 태그를 삽입해보자

  public void testHighlighting() throws Exception {
    String text = "The quick brown fox jumps over the lazy dog";

    TermQuery query = new TermQuery(new Term("field", "fox"));

    TokenStream tokenStream =
        new SimpleAnalyzer().tokenStream("field",
            new StringReader(text));

    QueryScorer scorer = new QueryScorer(query, "field");
    Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
    Highlighter highlighter = new Highlighter(scorer);
    highlighter.setTextFragmenter(fragmenter);
    assertEquals("The quick brown <B>fox</B> jumps over the lazy dog",
        highlighter.getBestFragment(tokenStream, text));
  }

 

실행결과

8.3.3 CSS 하이라이팅

 

예제 8.2 CSS를 사용해 검색결과 하이라이팅

public class HighlightIt {
  private static final String text =
    "In this section we'll show you how to make the simplest " +
    "programmatic query, searching for a single term, and then " +
    "we'll see how to use QueryParser to accept textual queries. " +
    "In the sections that follow, we’ll take this simple example " +
    "further by detailing all the query types built into Lucene. " +
    "We begin with the simplest search of all: searching for all " +
    "documents that contain a single term.";

  public static void main(String[] args) throws Exception {

    if (args.length != 1) {
      System.err.println("Usage: HighlightIt <filename-out>");
      System.exit(-1);
    }

    String filename = args[0];
    
    //질의 생성
    String searchText = "term";
    QueryParser parser = new QueryParser(Version.LUCENE_30,   
                                         "f",                 
                                         new StandardAnalyzer(Version.LUCENE_30));
    Query query = parser.parse(searchText);
    
    //텀의 앞뒤에 연결할 태그 지정
    SimpleHTMLFormatter formatter =   
      new SimpleHTMLFormatter("<span class=\"highlight\">",
                              "</span>"); 
    //토큰 추출
    TokenStream tokens = new StandardAnalyzer(Version.LUCENE_30)
        .tokenStream("f", new StringReader(text)); 

    QueryScorer scorer = new QueryScorer(query, "f"); 

    Highlighter highlighter = new Highlighter(formatter, scorer);
    highlighter.setTextFragmenter(
                  new SimpleSpanFragmenter(scorer));

    //최적의 조각 3개 추출
    String result =
        highlighter.getBestFragments(tokens, text, 3, "..."); 

    //하이라이팅된 HTML 출력
    FileWriter writer = new FileWriter(filename); 
    writer.write("<html>"); 
    writer.write("<style>\n" + 
        ".highlight {\n" +
        " background: yellow;\n" +
        "}\n" + 
        "</style>");
    writer.write("<body>");
    writer.write(result);
    writer.write("</body></html>");
    writer.close();
  }
}

 

8.3.4 검색 결과 하이라이팅

 

예제 8.3 검색결과 하이라이팅

public void testHits() throws Exception {
    IndexSearcher searcher = new IndexSearcher(TestUtil.getBookIndexDirectory());
    TermQuery query = new TermQuery(new Term("title", "action"));
    TopDocs hits = searcher.search(query, 10);

    QueryScorer scorer = new QueryScorer(query, "title");
    Highlighter highlighter = new Highlighter(scorer);
    highlighter.setTextFragmenter(
                   new SimpleSpanFragmenter(scorer));
      
    Analyzer analyzer = new SimpleAnalyzer();
    
    for (ScoreDoc sd : hits.scoreDocs) {
      Document doc = searcher.doc(sd.doc);
      String title = doc.get("title");

      TokenStream stream = TokenSources.getAnyTokenStream(searcher.getIndexReader(),
                                                          sd.doc,
                                                          "title",
                                                          doc,
                                                          analyzer);
      String fragment =
          highlighter.getBestFragment(stream, title);

      System.out.println(fragment);
    }
  }

 

검색결과

TokenSources.getAnyTokenStream 메소드 : 먼저 색인에 텀벡터가 저장되어있는지 확인한다. 없다면 지정된 분석기를 사용해 원문을 다시 분석한다.

setMaxDocCharsToAnalyzee 메소드 : 하이라이터 기본설정인 50KB 이상의 글자를 지정하고자 할 경우 사용한다. 하이라이팅 속도가 느려질 수 있다.


8.4 FastVectorHighlighter

 

장점

하이라이팅 기능을 빠르게 처리한다.

N그램 분석기로 분석한 필드도 하이라이팅할 수 있다.

텀에 따라 다중 색깔 하이라이팅 기능을 제공한다.

텀 단위가 아닌 구문 단위로 태그를 추가할 수 있다.

 

단점

색인에 저장된 텀 벡터만을 사용하기 때문에 디스크 공간을 더 차지한다.

 

예제 8.4 FastVectorHighlighter를 사용한 하이라이팅

public class FastVectorHighlighterSample {

  //각 문장을 문서로 색인
  static final String[] DOCS = {
    "the quick brown fox jumps over the lazy dog",
    "the quick gold fox jumped over the lazy black dog",
    "the quick fox jumps over the black dog",
    "the red fox jumped over the lazy dark gray dog"
  };
  static final String QUERY = "quick OR fox OR \"lazy dog\"~1";  //실행할 질의
  static final String F = "f";
  static Directory dir = new RAMDirectory();
  static Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);

  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage: FastVectorHighlighterSample <filename>");
      System.exit(-1);
    }
    makeIndex();
    searchIndex(args[0]);
  }

  static void makeIndex() throws IOException {
    IndexWriter writer = new IndexWriter(dir, analyzer,
                                     true, MaxFieldLength.UNLIMITED);
    for(String d : DOCS){
      Document doc = new Document();
      doc.add(new Field(F, d, Store.YES, Index.ANALYZED,
                        TermVector.WITH_POSITIONS_OFFSETS));
      writer.addDocument(doc);
    }
    writer.close();
  }
  
  static void searchIndex(String filename) throws Exception {
    QueryParser parser = new QueryParser(Version.LUCENE_30,
                                         F, analyzer);
    Query query = parser.parse(QUERY);                                
    FastVectorHighlighter highlighter = getHighlighter(); //FastVectorHighlighter 인스턴스 생성
    FieldQuery fieldQuery = highlighter.getFieldQuery(query); //FieldQuery 인스턴스 생성
    IndexSearcher searcher = new IndexSearcher(dir);           
    TopDocs docs = searcher.search(query, 10);                       

    FileWriter writer = new FileWriter(filename);
    writer.write("<html>");
    writer.write("<body>");
    writer.write("<p>QUERY : " + QUERY + "</p>");
    for(ScoreDoc scoreDoc : docs.scoreDocs) {
      //하이라이팅 조각을 최적화
      String snippet = highlighter.getBestFragment(     
          fieldQuery, searcher.getIndexReader(),
          scoreDoc.doc, F, 100 );
      if (snippet != null) {
        writer.write(scoreDoc.doc + " : " + snippet + "<br/>");
      }
    }
    writer.write("</body></html>");
    writer.close();
    searcher.close();
  }
  
  static FastVectorHighlighter getHighlighter() {
    //FastVectorHighlighter 준비
    FragListBuilder fragListBuilder = new SimpleFragListBuilder();
    FragmentsBuilder fragmentBuilder =
      new ScoreOrderFragmentsBuilder(
        BaseFragmentsBuilder.COLORED_PRE_TAGS,
        BaseFragmentsBuilder.COLORED_POST_TAGS);
    return new FastVectorHighlighter(true, true,
        fragListBuilder, fragmentBuilder);
  }
}

8.5 검색어  추천

 

8.5.1 추천 검색어 후보 추출

1. 검색어 추천 모듈

- 검색 질의의 단위로 동작하기 때문에 텀이 여러개 있다면 텀을 독립적으로 다룬다.
가능한 검색어 후보를 추출하는데 사용된다.

 

2. '올바른' 단어 사전
색인의 특정 필드에 들어있는 모든 유일한 텀을 모아 사전으로 활용한다.

 

검색어 추천 모듈은 발음이 비슷한 단어 또는 N그램을 활용해 단어사전에서 추천 단어를 뽑는다.

예제 8.5 검색어 추천 색인 생성

public class CreateSpellCheckerIndex {

  public static void main(String[] args) throws IOException {

    if (args.length != 3) {
      System.out.println("Usage: java lia.tools.SpellCheckerTest SpellCheckerIndexDir IndexDir IndexField");
      System.exit(1);
    }

    String spellCheckDir = args[0];
    String indexDir = args[1];
    String indexField = args[2];

    System.out.println("Now build SpellChecker index...");
    Directory dir = FSDirectory.open(new File(spellCheckDir));
    SpellChecker spell = new SpellChecker(dir);     //SpellCeheck 인스턴스 생성
    long startTime = System.currentTimeMillis();
    
    Directory dir2 = FSDirectory.open(new File(indexDir));
    IndexReader r = IndexReader.open(dir2);     //IndexReader 준비
    try {
      spell.indexDictionary(new LuceneDictionary(r, indexField));  //모든 단어 추가
    } finally {
      r.close();
    }
    dir.close();
    dir2.close();
    long endTime = System.currentTimeMillis();
    System.out.println("  took " + (endTime-startTime) + " milliseconds");
  }
}

 

8.5.2 최적의 추천 단어 선택

N그램을 통해 확보한 추천 단어 집합에서 최적의 추천 단어를 선택해보자.

 

예제 8.6 검색어 추천 색인을 바탕으로 추천 단어 후보 목록을 뽑아낸다.

public class SpellCheckerExample {

  public static void main(String[] args) throws IOException {

    if (args.length != 2) {
      System.out.println("Usage: java lia.tools.SpellCheckerTest SpellCheckerIndexDir wordToRespell");
      System.exit(1);
    }

    String spellCheckDir = args[0];
    String wordToRespell = args[1];

    Directory dir = FSDirectory.open(new File(spellCheckDir));
    if (!IndexReader.indexExists(dir)) {
      System.out.println("\nERROR: No spellchecker index at path \"" +
                         spellCheckDir +
                         "\"; please run CreateSpellCheckerIndex first\n");
      System.exit(1);
    }
    SpellChecker spell = new SpellChecker(dir);  //SpellChecker 인스턴스 생성

    spell.setStringDistance(new LevensteinDistance());  //거리 계산 방법 지정
    //spell.setStringDistance(new JaroWinklerDistance());

    String[] suggestions = spell.suggestSimilar(wordToRespell, 5); //후보 목록 추출
    System.out.println(suggestions.length + " suggestions for '" + wordToRespell + "':");
    for (String suggestion : suggestions)
      System.out.println("  " + suggestion);
  }
}

최적의 추천 단어 선택 시 일반적으로 사용하는 지표는 FuzzyQuery가 비슷한 단어를 검색할 때 사용했던 편집거리이다.

혹은 윙클러(Jaro-Winkler) 거리를 구현하거나 직접 구현한 객체를 사용해도 좋다.

 

추천 단어로 적당한지 확인할 때 사용되는 메소드

- StringDistance.getDistance() : 거리 지표 값을 계산

- SpellChecker.suggestSimilar() : 추가 인자를 지정해 검색 질의의 텀보다 빈도수가 더 높은 단어만 추천 단어로 설정 가능

- SpellChecker.setAccuracy() : 추천 단어마다 최소한의 연관도를 지정

 

8.5.4 추천 기능을 개선할 아이디어

  • 대량의 검색 요청을 처리하고 있다면, 사용자의 검색 질의에 들어있는 텀을 사용해 추천 단어를 찾아내는 방법이 효과적이다.
  • 특정 텀에 대한 추천 단어를 찾을 때 다른 텀을 활용해 좀 더 최적의 결과를 찾을 수 있다.
  • 특정한 형식의 텀은 추천 단어에서 제외하거나, 최소한의 빈도수를 갖추고 있는 단어만 추천하는 등의 방법이 필요하다.
  • 사용자가 추천 단어를 만족하고 동의하는지 여부에 따라 검색어 추천 기능을 최적화할 수 있다.
  • 검색 조건에 사용자의 권한 등이 포함되는 경우, 검색어 추천 사전 역시 권한에 따라 별도로 관리되어야한다.
  • 추천 검색어를 찾아내고 화면에 표시해야 할지 여부를 파악하려면, 먼저 사용자가 입력한 검색어로 검색해서 결과를 확인해보자. 결과가 없거나 매우 적다면 추천 검색어로 다시 검색해보고 표시 여부를 결정할 수 있다.

8.6 특이한 Query

contrib 모듈의 queries 부분에는 루씬에 내장된 질의에 비해 독특하고 재미있는 기능을 제공하는 질의가 많다.

 

8.6.1 MoreLikeThis

- 특정 문서와 비슷한 문서를 찾아내는데 필요한 기능을 담고있다.

- 특정 문서에서 텀을 찾아낸 다음 비슷한 문서를 찾는 Query 객체를 만들어 낸다.

- IndexReader 인스턴스와 docID 값을 알고 있다면 저장된 필드나 텀 벡터를 불러와 해당 문서에 대한 텀을 찾을 수 있다.

 

8.6.2 FuzzyLikeThisQuery

- FuzzQuery와 MoreLikeThis 클래스의 기능을 하나로 합한 것과 같다.

- 임의의 문자열을 추가해 질의를 만들 수 있으며, 기본 설정으로 StandardAnalyzer를 사용한다.

- 분석한 결과로 받은 토큰은 FuzzyQuery와 같은 절차를 거쳐 추천 단어를 뽑는다.

 

8.6.3 BoostingQuery

- 기본 질의를 먼저 실행하지만, 보조 질의에 검색결과 문서의 중요도를 별도로 지정하는 기능을 제공한다.

- BooleanQuery에 NOT 조건으로 negativeQuery를 추가하는 경우와 비슷하지만, NOT 조건의 문서를 모두 무시하는 대신 중요도 점수만 낮춰 순위를 조절한다.

 

8.6.4 TermFilter

- 지정한 텀에 해당하는 문서만 결과에 포함하는 필터이다.

- TermRangeFilter처럼 일련의 연결된 텀을 지정하지 않고, addTerm 메소드를 호출해 원하는 텀을 하나씩 추가한 후 검색할 때 필터로 지정해서 사용한다.

 

8.6.5 DuplicateFilter

- 분석하지 않은 특정 필드의 값이 중복되지 않게 제한하는 필터이다.

- 동일한 필드를 갖고 있는 문서 중 색인에 가장 마지막에 추가도니 문서만 남겨두고 이전의 문서는 무시하는 등으로 활용된다.

 

8.6.6 RegexQuery

- 지정한 정규표현식에 해당하는 텀을 포함하는 모든 문서를 검색 결과로 받아온다.

- 기본적으로 java.util.regex 패키지의 자바 정규 표현식 문법을 지원하며, 필요한 경우 org.apache.regexp 패키지에서 제공하는 문법을 사용할 수 있다.

728x90
Comments