Solr WordDelimiterFilter + Lucene Highlighter

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

Ожидается: "протестировать 1500 это"

Наблюдаемый: "проверить 11500 это"

В настоящее время я не уверен, что 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 );
        }
    }
}

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


person Lucas    schedule 30.12.2010    source источник


Ответы (2)


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

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'

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

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

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

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();
    }
}
person Lucas    schedule 03.01.2011
comment
Вы отправили патч в список рассылки? Я тоже только что столкнулся с этой ошибкой и не был уверен, что это было. Было бы круто починить. - person dwlz; 11.01.2011
comment
Извини, Дэн, нет, не видел. Причина в том, что я не могу понять, где. Я даже не могу найти багзиллу для Lucene, не говоря уже о Lucene Highlighter. У вас есть адрес списка рассылки? Пожалуйста, напишите здесь, и когда я увижу это в следующий раз, я посмотрю, смогу ли я представить это предложение. - person Lucas; 21.01.2011
comment
issues.apache.org/jira/browse/Lucene и issues.apache.org/jira/browse/SOLR — похоже, это было решено три недели назад: issues.apache.org/jira/browse/LUCENE-2874 - person Gunnlaugur Briem; 10.02.2011

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

person Yuval F    schedule 02.01.2011
comment
Код использует один и тот же анализатор в обоих случаях. Вы можете видеть выше, что он на самом деле создается одним и тем же вспомогательным методом (getAnalyzer) как в конструкторе QueryParser (для токенизатора запроса), так и с помощью highlighter.getBestFragments (для текстового токенизатора). Это работает, за исключением случая с , как указывает этот вопрос. Я думаю, что нашел проблему, и это ошибка в Lucene Highlighter. Я опубликую ответ ниже. - person Lucas; 03.01.2011