001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.    
018 */
019package org.apache.wiki.search;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.log4j.Logger;
023import org.apache.lucene.analysis.Analyzer;
024import org.apache.lucene.analysis.TokenStream;
025import org.apache.lucene.document.Document;
026import org.apache.lucene.document.Field;
027import org.apache.lucene.document.StringField;
028import org.apache.lucene.document.TextField;
029import org.apache.lucene.index.DirectoryReader;
030import org.apache.lucene.index.IndexReader;
031import org.apache.lucene.index.IndexWriter;
032import org.apache.lucene.index.IndexWriterConfig;
033import org.apache.lucene.index.IndexWriterConfig.OpenMode;
034import org.apache.lucene.index.Term;
035import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
036import org.apache.lucene.queryparser.classic.ParseException;
037import org.apache.lucene.queryparser.classic.QueryParser;
038import org.apache.lucene.search.IndexSearcher;
039import org.apache.lucene.search.Query;
040import org.apache.lucene.search.ScoreDoc;
041import org.apache.lucene.search.TermQuery;
042import org.apache.lucene.search.highlight.Highlighter;
043import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
044import org.apache.lucene.search.highlight.QueryScorer;
045import org.apache.lucene.search.highlight.SimpleHTMLEncoder;
046import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
047import org.apache.lucene.store.Directory;
048import org.apache.lucene.store.NIOFSDirectory;
049import org.apache.wiki.InternalWikiException;
050import org.apache.wiki.WatchDog;
051import org.apache.wiki.WikiBackgroundThread;
052import org.apache.wiki.api.core.Attachment;
053import org.apache.wiki.api.core.Context;
054import org.apache.wiki.api.core.Engine;
055import org.apache.wiki.api.core.Page;
056import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
057import org.apache.wiki.api.exceptions.ProviderException;
058import org.apache.wiki.api.providers.PageProvider;
059import org.apache.wiki.api.providers.WikiProvider;
060import org.apache.wiki.api.search.SearchResult;
061import org.apache.wiki.api.spi.Wiki;
062import org.apache.wiki.attachment.AttachmentManager;
063import org.apache.wiki.auth.AuthorizationManager;
064import org.apache.wiki.auth.permissions.PagePermission;
065import org.apache.wiki.pages.PageManager;
066import org.apache.wiki.util.ClassUtil;
067import org.apache.wiki.util.FileUtil;
068import org.apache.wiki.util.TextUtil;
069
070import java.io.*;
071import java.lang.reflect.Constructor;
072import java.util.*;
073import java.util.concurrent.Executor;
074import java.util.concurrent.Executors;
075
076
077/**
078 *  Interface for the search providers that handle searching the Wiki
079 *
080 *  @since 2.2.21.
081 */
082public class LuceneSearchProvider implements SearchProvider {
083
084    protected static final Logger log = Logger.getLogger(LuceneSearchProvider.class);
085
086    private Engine m_engine;
087    private Executor searchExecutor;
088
089    // Lucene properties.
090
091    /** Which analyzer to use.  Default is StandardAnalyzer. */
092    public static final String PROP_LUCENE_ANALYZER      = "jspwiki.lucene.analyzer";
093    private static final String PROP_LUCENE_INDEXDELAY   = "jspwiki.lucene.indexdelay";
094    private static final String PROP_LUCENE_INITIALDELAY = "jspwiki.lucene.initialdelay";
095
096    private String m_analyzerClass = "org.apache.lucene.analysis.standard.ClassicAnalyzer";
097
098    private static final String LUCENE_DIR = "lucene";
099
100    /** These attachment file suffixes will be indexed. */
101    public static final String[] SEARCHABLE_FILE_SUFFIXES = new String[] { ".txt", ".ini", ".xml", ".html", "htm", ".mm", ".htm",
102                                                                           ".xhtml", ".java", ".c", ".cpp", ".php", ".asm", ".sh",
103                                                                           ".properties", ".kml", ".gpx", ".loc", ".md", ".xml" };
104
105    protected static final String LUCENE_ID            = "id";
106    protected static final String LUCENE_PAGE_CONTENTS = "contents";
107    protected static final String LUCENE_AUTHOR        = "author";
108    protected static final String LUCENE_ATTACHMENTS   = "attachment";
109    protected static final String LUCENE_PAGE_NAME     = "name";
110    protected static final String LUCENE_PAGE_KEYWORDS = "keywords";
111
112    private String m_luceneDirectory;
113    protected final List< Object[] > m_updates = Collections.synchronizedList( new ArrayList<>() );
114
115    /** Maximum number of fragments from search matches. */
116    private static final int MAX_FRAGMENTS = 3;
117
118    /** The maximum number of hits to return from searches. */
119    public static final int MAX_SEARCH_HITS = 99_999;
120    
121    private static final String PUNCTUATION_TO_SPACES = StringUtils.repeat(" ", TextUtil.PUNCTUATION_CHARS_ALLOWED.length() );
122
123    /**
124     *  {@inheritDoc}
125     */
126    @Override
127    public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException, IOException  {
128        m_engine = engine;
129        searchExecutor = Executors.newCachedThreadPool();
130
131        m_luceneDirectory = engine.getWorkDir()+File.separator+LUCENE_DIR;
132
133        final int initialDelay = TextUtil.getIntegerProperty( props, PROP_LUCENE_INITIALDELAY, LuceneUpdater.INITIAL_DELAY );
134        final int indexDelay   = TextUtil.getIntegerProperty( props, PROP_LUCENE_INDEXDELAY, LuceneUpdater.INDEX_DELAY );
135
136        m_analyzerClass = TextUtil.getStringProperty( props, PROP_LUCENE_ANALYZER, m_analyzerClass );
137        // FIXME: Just to be simple for now, we will do full reindex only if no files are in lucene directory.
138
139        final File dir = new File(m_luceneDirectory);
140        log.info("Lucene enabled, cache will be in: "+dir.getAbsolutePath());
141        try {
142            if( !dir.exists() ) {
143                dir.mkdirs();
144            }
145
146            if( !dir.exists() || !dir.canWrite() || !dir.canRead() ) {
147                log.error("Cannot write to Lucene directory, disabling Lucene: "+dir.getAbsolutePath());
148                throw new IOException( "Invalid Lucene directory." );
149            }
150
151            final String[] filelist = dir.list();
152            if( filelist == null ) {
153                throw new IOException( "Invalid Lucene directory: cannot produce listing: "+dir.getAbsolutePath());
154            }
155        } catch( final IOException e ) {
156            log.error("Problem while creating Lucene index - not using Lucene.", e);
157        }
158
159        // Start the Lucene update thread, which waits first
160        // for a little while before starting to go through
161        // the Lucene "pages that need updating".
162        final LuceneUpdater updater = new LuceneUpdater( m_engine, this, initialDelay, indexDelay );
163        updater.start();
164    }
165
166    /**
167     *  Returns the handling engine.
168     *
169     *  @return Current Engine
170     */
171    protected Engine getEngine()
172    {
173        return m_engine;
174    }
175
176    /**
177     *  Performs a full Lucene reindex, if necessary.
178     *
179     *  @throws IOException If there's a problem during indexing
180     */
181    protected void doFullLuceneReindex() throws IOException {
182        final File dir = new File(m_luceneDirectory);
183        final String[] filelist = dir.list();
184        if( filelist == null ) {
185            throw new IOException( "Invalid Lucene directory: cannot produce listing: "+dir.getAbsolutePath());
186        }
187
188        try {
189            if( filelist.length == 0 ) {
190                //
191                //  No files? Reindex!
192                //
193                final Date start = new Date();
194
195                log.info("Starting Lucene reindexing, this can take a couple of minutes...");
196
197                final Directory luceneDir = new NIOFSDirectory( dir.toPath() );
198                try( final IndexWriter writer = getIndexWriter( luceneDir ) ) {
199                    final Collection< Page > allPages = m_engine.getManager( PageManager.class ).getAllPages();
200                    for( final Page page : allPages ) {
201                        try {
202                            final String text = m_engine.getManager( PageManager.class ).getPageText( page.getName(), WikiProvider.LATEST_VERSION );
203                            luceneIndexPage( page, text, writer );
204                        } catch( final IOException e ) {
205                            log.warn( "Unable to index page " + page.getName() + ", continuing to next ", e );
206                        }
207                    }
208
209                    final Collection< Attachment > allAttachments = m_engine.getManager( AttachmentManager.class ).getAllAttachments();
210                    for( final Attachment att : allAttachments ) {
211                        try {
212                            final String text = getAttachmentContent( att.getName(), WikiProvider.LATEST_VERSION );
213                            luceneIndexPage( att, text, writer );
214                        } catch( final IOException e ) {
215                            log.warn( "Unable to index attachment " + att.getName() + ", continuing to next", e );
216                        }
217                    }
218
219                }
220
221                final Date end = new Date();
222                log.info( "Full Lucene index finished in " + (end.getTime() - start.getTime()) + " milliseconds." );
223            } else {
224                log.info("Files found in Lucene directory, not reindexing.");
225            }
226        } catch ( final IOException e ) {
227            log.error("Problem while creating Lucene index - not using Lucene.", e);
228        } catch ( final ProviderException e ) {
229            log.error("Problem reading pages while creating Lucene index (JSPWiki won't start.)", e);
230            throw new IllegalArgumentException("unable to create Lucene index");
231        } catch( final Exception e ) {
232            log.error("Unable to start lucene",e);
233        }
234
235    }
236
237    /**
238     *  Fetches the attachment content from the repository.
239     *  Content is flat text that can be used for indexing/searching or display
240     *  
241     *  @param attachmentName Name of the attachment.
242     *  @param version The version of the attachment.
243     *  
244     *  @return the content of the Attachment as a String.
245     */
246    protected String getAttachmentContent( final String attachmentName, final int version ) {
247        final AttachmentManager mgr = m_engine.getManager( AttachmentManager.class );
248        try {
249            final Attachment att = mgr.getAttachmentInfo( attachmentName, version );
250            //FIXME: Find out why sometimes att is null
251            if( att != null ) {
252                return getAttachmentContent( att );
253            }
254        } catch( final ProviderException e ) {
255            log.error("Attachment cannot be loaded", e);
256        }
257        return null;
258    }
259
260    /**
261     * @param att Attachment to get content for. Filename extension is used to determine the type of the attachment.
262     * @return String representing the content of the file.
263     * FIXME This is a very simple implementation of some text-based attachment, mainly used for testing.
264     * This should be replaced /moved to Attachment search providers or some other 'pluggable' way to search attachments
265     */
266    protected String getAttachmentContent( final Attachment att ) {
267        final AttachmentManager mgr = m_engine.getManager( AttachmentManager.class );
268        //FIXME: Add attachment plugin structure
269
270        final String filename = att.getFileName();
271
272        boolean searchSuffix = false;
273        for( final String suffix : SEARCHABLE_FILE_SUFFIXES ) {
274            if( filename.endsWith( suffix ) ) {
275                searchSuffix = true;
276                break;
277            }
278        }
279
280        String out = filename;
281        if( searchSuffix ) {
282            try( final InputStream attStream = mgr.getAttachmentStream( att ); final StringWriter sout = new StringWriter() ) {
283                FileUtil.copyContents( new InputStreamReader( attStream ), sout );
284                out = out + " " + sout.toString();
285            } catch( final ProviderException | IOException e ) {
286                log.error("Attachment cannot be loaded", e);
287            }
288        }
289
290        return out;
291    }
292
293    /**
294     *  Updates the lucene index for a single page.
295     *
296     *  @param page The WikiPage to check
297     *  @param text The page text to index.
298     */
299    protected synchronized void updateLuceneIndex( final Page page, final String text ) {
300        log.debug("Updating Lucene index for page '" + page.getName() + "'...");
301        pageRemoved( page );
302
303        // Now add back the new version.
304        try( final Directory luceneDir = new NIOFSDirectory( new File( m_luceneDirectory ).toPath() );
305             final IndexWriter writer = getIndexWriter( luceneDir ) ) {
306            luceneIndexPage( page, text, writer );
307        } catch( final IOException e ) {
308            log.error("Unable to update page '" + page.getName() + "' from Lucene index", e);
309            // reindexPage( page );
310        } catch( final Exception e ) {
311            log.error("Unexpected Lucene exception - please check configuration!",e);
312            // reindexPage( page );
313        }
314
315        log.debug("Done updating Lucene index for page '" + page.getName() + "'.");
316    }
317
318    private Analyzer getLuceneAnalyzer() throws ProviderException {
319        try {
320            final Class< ? > clazz = ClassUtil.findClass( "", m_analyzerClass );
321            final Constructor< ? > constructor = clazz.getConstructor();
322            return ( Analyzer )constructor.newInstance();
323        } catch( final Exception e ) {
324            final String msg = "Could not get LuceneAnalyzer class " + m_analyzerClass + ", reason: ";
325            log.error( msg, e );
326            throw new ProviderException( msg + e );
327        }
328    }
329
330    /**
331     *  Indexes page using the given IndexWriter.
332     *
333     *  @param page WikiPage
334     *  @param text Page text to index
335     *  @param writer The Lucene IndexWriter to use for indexing
336     *  @return the created index Document
337     *  @throws IOException If there's an indexing problem
338     */
339    protected Document luceneIndexPage( final Page page, final String text, final IndexWriter writer ) throws IOException {
340        if( log.isDebugEnabled() ) {
341            log.debug( "Indexing " + page.getName() + "..." );
342        }
343        
344        // make a new, empty document
345        final Document doc = new Document();
346
347        if( text == null ) {
348            return doc;
349        }
350        final String indexedText = text.replace( "__", " " ); // be nice to Language Analyzers - cfr. JSPWIKI-893
351
352        // Raw name is the keyword we'll use to refer to this document for updates.
353        Field field = new Field( LUCENE_ID, page.getName(), StringField.TYPE_STORED );
354        doc.add( field );
355
356        // Body text.  It is stored in the doc for search contexts.
357        field = new Field( LUCENE_PAGE_CONTENTS, indexedText, TextField.TYPE_STORED );
358        doc.add( field );
359
360        // Allow searching by page name. Both beautified and raw
361        final String unTokenizedTitle = StringUtils.replaceChars( page.getName(), TextUtil.PUNCTUATION_CHARS_ALLOWED, PUNCTUATION_TO_SPACES );
362        field = new Field( LUCENE_PAGE_NAME, TextUtil.beautifyString( page.getName() ) + " " + unTokenizedTitle, TextField.TYPE_STORED );
363        doc.add( field );
364
365        // Allow searching by authorname
366        if( page.getAuthor() != null ) {
367            field = new Field( LUCENE_AUTHOR, page.getAuthor(), TextField.TYPE_STORED );
368            doc.add( field );
369        }
370
371        // Now add the names of the attachments of this page
372        try {
373            final List< Attachment > attachments = m_engine.getManager( AttachmentManager.class ).listAttachments( page );
374            final StringBuilder attachmentNames = new StringBuilder();
375
376            for( final Attachment att : attachments ) {
377                attachmentNames.append( att.getName() ).append( ";" );
378            }
379            field = new Field( LUCENE_ATTACHMENTS, attachmentNames.toString(), TextField.TYPE_STORED );
380            doc.add( field );
381
382        } catch( final ProviderException e ) {
383            // Unable to read attachments
384            log.error( "Failed to get attachments for page", e );
385        }
386
387        // also index page keywords, if available
388        if( page.getAttribute( "keywords" ) != null ) {
389            field = new Field( LUCENE_PAGE_KEYWORDS, page.getAttribute( "keywords" ).toString(), TextField.TYPE_STORED );
390            doc.add( field );
391        }
392        synchronized( writer ) {
393            writer.addDocument(doc);
394        }
395
396        return doc;
397    }
398
399    /**
400     *  {@inheritDoc}
401     */
402    @Override
403    public synchronized void pageRemoved( final Page page ) {
404        try( final Directory luceneDir = new NIOFSDirectory( new File( m_luceneDirectory ).toPath() );
405             final IndexWriter writer = getIndexWriter( luceneDir ) ) {
406            final Query query = new TermQuery( new Term( LUCENE_ID, page.getName() ) );
407            writer.deleteDocuments( query );
408        } catch ( final Exception e ) {
409            log.error("Unable to remove page '" + page.getName() + "' from Lucene index", e);
410        }
411    }
412    
413    IndexWriter getIndexWriter(final  Directory luceneDir ) throws IOException, ProviderException {
414        final IndexWriterConfig writerConfig = new IndexWriterConfig( getLuceneAnalyzer() );
415        writerConfig.setOpenMode( OpenMode.CREATE_OR_APPEND );
416        return new IndexWriter( luceneDir, writerConfig );
417    }
418    
419    /**
420     *  Adds a page-text pair to the lucene update queue.  Safe to call always
421     *
422     *  @param page WikiPage to add to the update queue.
423     */
424    @Override
425    public void reindexPage( final Page page ) {
426        if( page != null ) {
427            final String text;
428
429            // TODO: Think if this was better done in the thread itself?
430            if( page instanceof Attachment ) {
431                text = getAttachmentContent( ( Attachment )page );
432            } else {
433                text = m_engine.getManager( PageManager.class ).getPureText( page );
434            }
435
436            if( text != null ) {
437                // Add work item to m_updates queue.
438                final Object[] pair = new Object[2];
439                pair[0] = page;
440                pair[1] = text;
441                m_updates.add( pair );
442                log.debug("Scheduling page " + page.getName() + " for index update");
443            }
444        }
445    }
446
447    /**
448     *  {@inheritDoc}
449     */
450    @Override
451    public Collection< SearchResult > findPages( final String query, final Context wikiContext ) throws ProviderException {
452        return findPages( query, FLAG_CONTEXTS, wikiContext );
453    }
454
455    /**
456     *  Create contexts also.  Generating contexts can be expensive,
457     *  so they're not on by default.
458     */
459    public static final int FLAG_CONTEXTS = 0x01;
460
461    /**
462     *  Searches pages using a particular combination of flags.
463     *
464     *  @param query The query to perform in Lucene query language
465     *  @param flags A set of flags
466     *  @return A Collection of SearchResult instances
467     *  @throws ProviderException if there is a problem with the backend
468     */
469    public Collection< SearchResult > findPages( final String query, final int flags, final Context wikiContext ) throws ProviderException {
470        ArrayList<SearchResult> list = null;
471        Highlighter highlighter = null;
472
473        try( final Directory luceneDir = new NIOFSDirectory( new File( m_luceneDirectory ).toPath() );
474             final IndexReader reader = DirectoryReader.open( luceneDir ) ) {
475            final String[] queryfields = { LUCENE_PAGE_CONTENTS, LUCENE_PAGE_NAME, LUCENE_AUTHOR, LUCENE_ATTACHMENTS, LUCENE_PAGE_KEYWORDS };
476            final QueryParser qp = new MultiFieldQueryParser( queryfields, getLuceneAnalyzer() );
477            final Query luceneQuery = qp.parse( query );
478            final IndexSearcher searcher = new IndexSearcher( reader, searchExecutor );
479
480            if( (flags & FLAG_CONTEXTS) != 0 ) {
481                highlighter = new Highlighter(new SimpleHTMLFormatter("<span class=\"searchmatch\">", "</span>"),
482                                              new SimpleHTMLEncoder(),
483                                              new QueryScorer( luceneQuery ) );
484            }
485
486            final ScoreDoc[] hits = searcher.search(luceneQuery, MAX_SEARCH_HITS).scoreDocs;
487            final AuthorizationManager mgr = m_engine.getManager( AuthorizationManager.class );
488
489            list = new ArrayList<>(hits.length);
490            for( final ScoreDoc hit : hits ) {
491                final int docID = hit.doc;
492                final Document doc = searcher.doc( docID );
493                final String pageName = doc.get( LUCENE_ID );
494                final Page page = m_engine.getManager( PageManager.class ).getPage( pageName, PageProvider.LATEST_VERSION );
495
496                if( page != null ) {
497                    final PagePermission pp = new PagePermission( page, PagePermission.VIEW_ACTION );
498                    if( mgr.checkPermission( wikiContext.getWikiSession(), pp ) ) {
499                        final int score = ( int )( hit.score * 100 );
500
501                        // Get highlighted search contexts
502                        final String text = doc.get( LUCENE_PAGE_CONTENTS );
503
504                        String[] fragments = new String[ 0 ];
505                        if( text != null && highlighter != null ) {
506                            final TokenStream tokenStream = getLuceneAnalyzer()
507                                    .tokenStream( LUCENE_PAGE_CONTENTS, new StringReader( text ) );
508                            fragments = highlighter.getBestFragments( tokenStream, text, MAX_FRAGMENTS );
509                        }
510
511                        final SearchResult result = new SearchResultImpl( page, score, fragments );
512                        list.add( result );
513                    }
514                } else {
515                    log.error( "Lucene found a result page '" + pageName + "' that could not be loaded, removing from Lucene cache" );
516                    pageRemoved( Wiki.contents().page( m_engine, pageName ) );
517                }
518            }
519        } catch( final IOException e ) {
520            log.error("Failed during lucene search",e);
521        } catch( final ParseException e ) {
522            log.info("Broken query; cannot parse query: " + query, e);
523            throw new ProviderException( "You have entered a query Lucene cannot process [" + query + "]: " + e.getMessage() );
524        } catch( final InvalidTokenOffsetsException e ) {
525            log.error("Tokens are incompatible with provided text ",e);
526        }
527
528        return list;
529    }
530
531    /**
532     *  {@inheritDoc}
533     */
534    @Override
535    public String getProviderInfo()
536    {
537        return "LuceneSearchProvider";
538    }
539
540    /**
541     * Updater thread that updates Lucene indexes.
542     */
543    private static final class LuceneUpdater extends WikiBackgroundThread {
544        protected static final int INDEX_DELAY    = 5;
545        protected static final int INITIAL_DELAY = 60;
546        private final LuceneSearchProvider m_provider;
547
548        private int m_initialDelay;
549
550        private WatchDog m_watchdog;
551
552        private LuceneUpdater( final Engine engine, final LuceneSearchProvider provider, final int initialDelay, final int indexDelay ) {
553            super( engine, indexDelay );
554            m_provider = provider;
555            m_initialDelay = initialDelay;
556            setName("JSPWiki Lucene Indexer");
557        }
558
559        @Override
560        public void startupTask() throws Exception {
561            m_watchdog = WatchDog.getCurrentWatchDog( getEngine() );
562
563            // Sleep initially...
564            try {
565                Thread.sleep( m_initialDelay * 1000L );
566            } catch( final InterruptedException e ) {
567                throw new InternalWikiException("Interrupted while waiting to start.", e);
568            }
569
570            m_watchdog.enterState( "Full reindex" );
571            // Reindex everything
572            m_provider.doFullLuceneReindex();
573            m_watchdog.exitState();
574        }
575
576        @Override
577        public void backgroundTask() {
578            m_watchdog.enterState("Emptying index queue", 60);
579
580            synchronized ( m_provider.m_updates ) {
581                while( m_provider.m_updates.size() > 0 ) {
582                    final Object[] pair = m_provider.m_updates.remove(0);
583                    final Page page = ( Page ) pair[0];
584                    final String text = ( String ) pair[1];
585                    m_provider.updateLuceneIndex(page, text);
586                }
587            }
588
589            m_watchdog.exitState();
590        }
591
592    }
593
594    // FIXME: This class is dumb; needs to have a better implementation
595    private static class SearchResultImpl implements SearchResult {
596
597        private Page m_page;
598        private int      m_score;
599        private String[] m_contexts;
600
601        public SearchResultImpl( final Page page, final int score, final String[] contexts ) {
602            m_page = page;
603            m_score = score;
604            m_contexts = contexts != null ? contexts.clone() : null;
605        }
606
607        @Override
608        public Page getPage()
609        {
610            return m_page;
611        }
612
613        /* (non-Javadoc)
614         * @see org.apache.wiki.SearchResult#getScore()
615         */
616        @Override
617        public int getScore()
618        {
619            return m_score;
620        }
621
622
623        @Override
624        public String[] getContexts()
625        {
626            return m_contexts;
627        }
628    }
629
630}