Solr WordDelimiterFilter + Lucene Highlighter - PullRequest
       23

Solr WordDelimiterFilter + Lucene Highlighter

1 голос
/ 31 декабря 2010

Я пытаюсь заставить класс Highlighter из Lucene корректно работать с токенами, поступающими из Solr WordDelimiterFilter.Он работает 90% времени, но если соответствующий текст содержит «,», например «1500», вывод неправильный:

Ожидаемый: «тест 1500 this»

Замечено: 'test 1 1,500 this'

В настоящее время я не уверен, что это Highlighter испортил рекомбинацию или WordDelimiterFilter испортил токенизацию, но что-тонесчастенВот соответствующие зависимости от моего pom:

org.apache.lucene lucene-core 2.9.3 jar compile org.apache.lucene lucene-highlighter 2.9.3 jar compile org.apache.solr solr-core 1.4.0 jar compile

А вот простой тестовый класс JUnit, демонстрирующий проблему:

package test.lucene;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;


import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;


import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleFragmenter;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.util.Version;
import org.apache.solr.analysis.StandardTokenizerFactory;
import org.apache.solr.analysis.WordDelimiterFilterFactory;
import org.junit.Test;


public class HighlighterTester {
    private static final String PRE_TAG = "<b>";
    private static final String POST_TAG = "</b>";

    private static String[] highlightField( Query query, String fieldName, String text )
            throws IOException, InvalidTokenOffsetsException {
        SimpleHTMLFormatter formatter = new SimpleHTMLFormatter( PRE_TAG, POST_TAG );
        Highlighter highlighter = new Highlighter( formatter, new QueryScorer( query, fieldName ) );
        highlighter.setTextFragmenter( new SimpleFragmenter( Integer.MAX_VALUE ) );
        return highlighter.getBestFragments( getAnalyzer(), fieldName, text, 10 );
    }

    private static Analyzer getAnalyzer() {
        return new Analyzer() {
            @Override
            public TokenStream tokenStream( String fieldName, Reader reader ) {
                // Start with a StandardTokenizer
                TokenStream stream = new StandardTokenizerFactory().create( reader );

                // Chain on a WordDelimiterFilter
                WordDelimiterFilterFactory wordDelimiterFilterFactory = new WordDelimiterFilterFactory();
                HashMap<String, String> arguments = new HashMap<String, String>();
                arguments.put( "generateWordParts", "1" );
                arguments.put( "generateNumberParts", "1" );
                arguments.put( "catenateWords", "1" );
                arguments.put( "catenateNumbers", "1" );
                arguments.put( "catenateAll", "0" );
                wordDelimiterFilterFactory.init( arguments );

                return wordDelimiterFilterFactory.create( stream );
            }
        };
    }

    @Test
    public void TestHighlighter() throws ParseException, IOException, InvalidTokenOffsetsException {
        String fieldName = "text";
        String text = "test 1,500 this";
        String queryString = "1500";
        String expected = "test " + PRE_TAG + "1,500" + POST_TAG + " this";

        QueryParser parser = new QueryParser( Version.LUCENE_29, fieldName, getAnalyzer() );
        Query q = parser.parse( queryString );
        String[] observed = highlightField( q, fieldName, text );
        for ( int i = 0; i < observed.length; i++ ) {
            System.out.println( "\t" + i + ": '" + observed[i] + "'" );
        }
        if ( observed.length > 0 ) {
            System.out.println( "Expected: '" + expected + "'\n" + "Observed: '" + observed[0] + "'" );
            assertEquals( expected, observed[0] );
        }
        else {
            assertTrue( "No matches found", false );
        }
    }
}

У кого-нибудь есть идеи или предложения?

Ответы [ 2 ]

2 голосов
/ 03 января 2011

После дальнейшего изучения это, похоже, ошибка в коде подсветки Lucene.Как вы можете видеть здесь:

public class TokenGroup {

    ...

    protected boolean isDistinct() {
        return offsetAtt.startOffset() >= endOffset;
    }

    ...

Код пытается определить, отличается ли группа токенов, проверяя, является ли начальное смещение большим, чем предыдущее конечное смещение.Проблема с этим подходом иллюстрируется этой проблемой.Если бы вам пришлось пройти по токенам, вы бы увидели, что они выглядят следующим образом:

0-4: 'test', 'test'
5-6: '1', '1'
7-10: '500', '500'
5-10: '1500', '1,500'
11-15: 'this', 'this'

Из этого вы можете видеть, что третий токен начинается после окончания второго, но четвертый запускает тот жеместо как второе.Предполагаемый результат - сгруппировать токены 2, 3 и 4, но согласно этой реализации токен 3 рассматривается отдельно от 2, поэтому 2 отображается сам по себе, затем 3 и 4 группируются, оставляя этот результат:

Expected: 'test <b>1,500</b> this'
Observed: 'test 1<b>1,500</b> this'

Я не уверен, что это может быть достигнуто без 2 проходов, один для получения всех индексов и второй для их объединения.Кроме того, я не уверен, какие последствия будут за пределами этого конкретного случая.У кого-нибудь есть здесь идеи?

РЕДАКТИРОВАТЬ

Вот окончательный исходный код, который я придумал.Это сгруппирует вещи правильно.Это также кажется НАМНОГО проще, чем реализация Lucene Highlighter, но по общему признанию не обрабатывает различные уровни скоринга, поскольку моему приложению нужно только да / нет относительно того, выделен ли фрагмент текста.Стоит также отметить, что я использую их QueryScorer для оценки текстовых фрагментов, которые имеют слабую ориентацию на термин, а не на фразу, что означает, что строка поиска «грамматическая или орфографическая» в конечном итоге будет выделена примерно так: »1014 * грамматическое или правописание"как или, скорее всего, будет отброшено вашим анализатором.Во всяком случае, вот мой источник:

public TextFragments<E> getTextFragments( TokenStream tokenStream,
        String text,
        Scorer scorer )
        throws IOException, InvalidTokenOffsetsException {
    OffsetAttribute offsetAtt = (OffsetAttribute) tokenStream.addAttribute( OffsetAttribute.class );
    TermAttribute termAtt = (TermAttribute) tokenStream.addAttribute( TermAttribute.class );
    TokenStream newStream = scorer.init( tokenStream );
    if ( newStream != null ) {
        tokenStream = newStream;
    }

    TokenGroups tgs = new TokenGroups();
    scorer.startFragment( null );
    while ( tokenStream.incrementToken() ) {
        tgs.add( offsetAtt.startOffset(), offsetAtt.endOffset(), scorer.getTokenScore() );
        if ( log.isTraceEnabled() ) {
            log.trace( new StringBuilder()
                    .append( scorer.getTokenScore() )
                    .append( " " )
                    .append( offsetAtt.startOffset() )
                    .append( "-" )
                    .append( offsetAtt.endOffset() )
                    .append( ": '" )
                    .append( termAtt.term() )
                    .append( "', '" )
                    .append( text.substring( offsetAtt.startOffset(), offsetAtt.endOffset() ) )
                    .append( "'" )
                    .toString() );
        }
    }

    return tgs.fragment( text );
}

private class TokenGroup {
    private int startIndex;
    private int endIndex;
    private float score;

    public TokenGroup( int startIndex, int endIndex, float score ) {
        this.startIndex = startIndex;
        this.endIndex = endIndex;
        this.score = score;
    }
}

private class TokenGroups implements Iterable<TokenGroup> {
    private List<TokenGroup> tgs;

    public TokenGroups() {
        tgs = new ArrayList<TokenGroup>();
    }

    public void add( int startIndex, int endIndex, float score ) {
        add( new TokenGroup( startIndex, endIndex, score ) );
    }

    public void add( TokenGroup tg ) {
        for ( int i = tgs.size() - 1; i >= 0; i-- ) {
            if ( tg.startIndex < tgs.get( i ).endIndex ) {
                tg = merge( tg, tgs.remove( i ) );
            }
            else {
                break;
            }
        }
        tgs.add( tg );
    }

    private TokenGroup merge( TokenGroup tg1, TokenGroup tg2 ) {
        return new TokenGroup( Math.min( tg1.startIndex, tg2.startIndex ),
                Math.max( tg1.endIndex, tg2.endIndex ),
                Math.max( tg1.score, tg2.score ) );
    }

    private TextFragments<E> fragment( String text ) {
        TextFragments<E> fragments = new TextFragments<E>();

        int lastEndIndex = 0;
        for ( TokenGroup tg : this ) {
            if ( tg.startIndex > lastEndIndex ) {
                fragments.add( text.substring( lastEndIndex, tg.startIndex ), textModeNormal );
            }
            fragments.add( 
                    text.substring( tg.startIndex, tg.endIndex ),
                    tg.score > 0 ? textModeHighlighted : textModeNormal );
            lastEndIndex = tg.endIndex;
        }

        if ( lastEndIndex < ( text.length() - 1 ) ) {
            fragments.add( text.substring( lastEndIndex ), textModeNormal );
        }

        return fragments;
    }

    @Override
    public Iterator<TokenGroup> iterator() {
        return tgs.iterator();
    }
}
0 голосов
/ 02 января 2011

Вот возможная причина. Ваш маркер должен использовать тот же Анализатор, который использовался для поиска. IIUC, Ваш код использует анализатор по умолчанию для выделения, хотя он использует специализированный анализатор для анализа запроса. Я считаю, что вам нужно изменить Fragmenter для работы с вашим конкретным TokenStream.

...