【问题标题】:Solr WordDelimiterFilter + Lucene HighlighterSolr WordDelimiterFilter + Lucene 荧光笔
【发布时间】:2011-06-01 18:42:01
【问题描述】:

我正在尝试让来自 Lucene 的 Highlighter 类与来自 Solr 的 WordDelimiterFilter 的标记一起正常工作。它在 90% 的情况下都有效,但如果匹配文本包含“,”,例如“1,500”,则输出不正确:

预期:'测试 1,500 这个'

观察到:'测试 11,500 这个'

我目前不确定是 Highlighter 搞砸了重组还是 WordDelimiterFilter 搞砸了标记化,但有些不开心。以下是我的 pom 中的相关依赖项:

org.apache.lucene lucene核心 2.9.3 罐 编译 org.apache.lucene lucene 荧光笔 2.9.3 罐 编译 org.apache.solr solr-核心 1.4.0 罐 编译

这里有一个简单的 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 );
        }
    }
}

有人有什么想法或建议吗?

【问题讨论】:

    标签: java solr lucene tokenize lucene-highlighter


    【解决方案1】:

    经过进一步调查,这似乎是 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'
    

    我不确定这是否可以在没有 2 次通过的情况下完成,一次获取所有索引,第二次合并它们。另外,我不确定在这个特定案例之外会产生什么影响。有人在这里有什么想法吗?

    编辑

    这是我想出的最终源代码。它将正确地分组。它似乎也比 Lucene Highlighter 实现简单得多,但不可否认,它不能处理不同级别的评分,因为我的应用程序只需要一个是/否来确定是否突出显示文本片段。还值得注意的是,我正在使用他们的 QueryScorer 对文本片段进行评分,这些文本片段确实具有面向术语而不是面向短语的弱点,这意味着搜索字符串“语法或拼写”最终会突出显示看起来像这样“grammatical 或 spelling" 作为 or 很可能会被您的分析器丢弃。无论如何,这是我的来源:

    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();
        }
    }
    

    【讨论】:

    【解决方案2】:

    这是一个可能的原因。 您的荧光笔需要使用与搜索相同的分析器。 IIUC,您的代码使用默认分析器进行突出显示,即使它使用专门的分析器来解析查询。我相信您需要更改 Fragmenter 以使用您的特定 TokenStream。

    【讨论】:

    • 代码在两种情况下都使用相同的分析器。您可以在上面看到,它实际上是由 QueryParser 构造函数(用于查询标记器)和 highlighter.getBestFragments(用于文本标记器)中的相同辅助方法 (getAnalyzer) 创建的。正如这个问题所表明的,除了在 的情况下,这有效。我确实认为我已经找到了问题,这是 Lucene Highlighter 中的一个错误。我将在下面发布答案。
    猜你喜欢
    • 1970-01-01
    • 2012-07-31
    • 2015-02-24
    • 2011-03-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-25
    • 1970-01-01
    • 2014-04-11
    相关资源
    最近更新 更多