View Javadoc
1   package org.apache.maven.index;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0    
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Comparator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.TreeMap;
33  import java.util.TreeSet;
34  
35  import org.apache.lucene.document.Document;
36  import org.apache.lucene.search.IndexSearcher;
37  import org.apache.lucene.search.Query;
38  import org.apache.lucene.search.ScoreDoc;
39  import org.apache.lucene.search.TopScoreDocCollector;
40  import org.apache.maven.index.context.IndexUtils;
41  import org.apache.maven.index.context.IndexingContext;
42  import org.apache.maven.index.context.NexusIndexMultiReader;
43  import org.apache.maven.index.context.NexusIndexMultiSearcher;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * A default search engine implementation
49   * 
50   * @author Eugene Kuleshov
51   * @author Tamas Cservenak
52   */
53  @Singleton
54  @Named
55  public class DefaultSearchEngine
56      implements SearchEngine
57  {
58  
59      private final Logger logger = LoggerFactory.getLogger( getClass() );
60  
61      protected Logger getLogger()
62      {
63          return logger;
64      }
65  
66      @Deprecated
67      public Set<ArtifactInfo> searchFlat( Comparator<ArtifactInfo> artifactInfoComparator,
68                                           IndexingContext indexingContext, Query query )
69          throws IOException
70      {
71          return searchFlatPaged( new FlatSearchRequest( query, artifactInfoComparator, indexingContext ),
72              Arrays.asList( indexingContext ), true ).getResults();
73      }
74  
75      @Deprecated
76      public Set<ArtifactInfo> searchFlat( Comparator<ArtifactInfo> artifactInfoComparator,
77                                           Collection<IndexingContext> indexingContexts, Query query )
78          throws IOException
79      {
80          return searchFlatPaged( new FlatSearchRequest( query, artifactInfoComparator ), indexingContexts ).getResults();
81      }
82  
83      public FlatSearchResponse searchFlatPaged( FlatSearchRequest request, Collection<IndexingContext> indexingContexts )
84          throws IOException
85      {
86          return searchFlatPaged( request, indexingContexts, false );
87      }
88  
89      public FlatSearchResponse forceSearchFlatPaged( FlatSearchRequest request,
90                                                      Collection<IndexingContext> indexingContexts )
91          throws IOException
92      {
93          return searchFlatPaged( request, indexingContexts, true );
94      }
95  
96      protected FlatSearchResponse searchFlatPaged( FlatSearchRequest request,
97                                                    Collection<IndexingContext> indexingContexts, boolean ignoreContext )
98          throws IOException
99      {
100         List<IndexingContext> contexts = getParticipatingContexts( indexingContexts, ignoreContext );
101 
102         final TreeSet<ArtifactInfo> result = new TreeSet<>( request.getArtifactInfoComparator() );
103         return new FlatSearchResponse( request.getQuery(), searchFlat( request, result, contexts, request.getQuery() ),
104             result );
105     }
106 
107     // ==
108 
109     public GroupedSearchResponse searchGrouped( GroupedSearchRequest request,
110                                                 Collection<IndexingContext> indexingContexts )
111         throws IOException
112     {
113         return searchGrouped( request, indexingContexts, false );
114     }
115 
116     public GroupedSearchResponse forceSearchGrouped( GroupedSearchRequest request,
117                                                      Collection<IndexingContext> indexingContexts )
118         throws IOException
119     {
120         return searchGrouped( request, indexingContexts, true );
121     }
122 
123     protected GroupedSearchResponse searchGrouped( GroupedSearchRequest request,
124                                                    Collection<IndexingContext> indexingContexts, boolean ignoreContext )
125         throws IOException
126     {
127         List<IndexingContext> contexts = getParticipatingContexts( indexingContexts, ignoreContext );
128 
129         final TreeMap<String, ArtifactInfoGroup> result = new TreeMap<>( request.getGroupKeyComparator() );
130 
131         return new GroupedSearchResponse( request.getQuery(), searchGrouped( request, result, request.getGrouping(),
132             contexts, request.getQuery() ), result );
133     }
134 
135     // ===
136 
137     protected int searchFlat( FlatSearchRequest req, Collection<ArtifactInfo> result,
138                               List<IndexingContext> participatingContexts, Query query )
139         throws IOException
140     {
141         int hitCount = 0;
142         for ( IndexingContext context : participatingContexts )
143         {
144             final IndexSearcher indexSearcher = context.acquireIndexSearcher();
145             try
146             {
147                 final TopScoreDocCollector collector = doSearchWithCeiling( req, indexSearcher, query );
148 
149                 if ( collector.getTotalHits() == 0 )
150                 {
151                     // context has no hits, just continue to next one
152                     continue;
153                 }
154 
155                 ScoreDoc[] scoreDocs = collector.topDocs().scoreDocs;
156 
157                 // uhm btw hitCount contains dups
158 
159                 hitCount += collector.getTotalHits();
160 
161                 int start = 0; // from == FlatSearchRequest.UNDEFINED ? 0 : from;
162 
163                 // we have to pack the results as long: a) we have found aiCount ones b) we depleted hits
164                 for ( int i = start; i < scoreDocs.length; i++ )
165                 {
166                     Document doc = indexSearcher.doc( scoreDocs[i].doc );
167 
168                     ArtifactInfo artifactInfo = IndexUtils.constructArtifactInfo( doc, context );
169 
170                     if ( artifactInfo != null )
171                     {
172                         artifactInfo.setRepository( context.getRepositoryId() );
173                         artifactInfo.setContext( context.getId() );
174 
175                         if ( req.getArtifactInfoFilter() != null )
176                         {
177                             if ( !req.getArtifactInfoFilter().accepts( context, artifactInfo ) )
178                             {
179                                 continue;
180                             }
181                         }
182                         if ( req.getArtifactInfoPostprocessor() != null )
183                         {
184                             req.getArtifactInfoPostprocessor().postprocess( context, artifactInfo );
185                         }
186 
187                         result.add( artifactInfo );
188                     }
189                 }
190             }
191             finally
192             {
193                 context.releaseIndexSearcher( indexSearcher );
194             }
195         }
196 
197         return hitCount;
198     }
199 
200     protected int searchGrouped( GroupedSearchRequest req, Map<String, ArtifactInfoGroup> result, Grouping grouping,
201                                  List<IndexingContext> participatingContexts, Query query )
202         throws IOException
203     {
204         int hitCount = 0;
205 
206         for ( IndexingContext context : participatingContexts )
207         {
208             final IndexSearcher indexSearcher = context.acquireIndexSearcher();
209             try
210             {
211                 final TopScoreDocCollector collector = doSearchWithCeiling( req, indexSearcher, query );
212 
213                 if ( collector.getTotalHits() > 0 )
214                 {
215                     ScoreDoc[] scoreDocs = collector.topDocs().scoreDocs;
216 
217                     hitCount += collector.getTotalHits();
218 
219                     for ( ScoreDoc scoreDoc : scoreDocs )
220                     {
221                         Document doc = indexSearcher.doc( scoreDoc.doc );
222 
223                         ArtifactInfo artifactInfo = IndexUtils.constructArtifactInfo( doc, context );
224 
225                         if ( artifactInfo != null )
226                         {
227                             artifactInfo.setRepository( context.getRepositoryId() );
228                             artifactInfo.setContext( context.getId() );
229 
230                             if ( req.getArtifactInfoFilter() != null )
231                             {
232                                 if ( !req.getArtifactInfoFilter().accepts( context, artifactInfo ) )
233                                 {
234                                     continue;
235                                 }
236                             }
237                             if ( req.getArtifactInfoPostprocessor() != null )
238                             {
239                                 req.getArtifactInfoPostprocessor().postprocess( context, artifactInfo );
240                             }
241 
242                             if ( !grouping.addArtifactInfo( result, artifactInfo ) )
243                             {
244                                 // fix the hitCount accordingly
245                                 hitCount--;
246                             }
247                         }
248                     }
249                 }
250             }
251             finally
252             {
253                 context.releaseIndexSearcher( indexSearcher );
254             }
255         }
256 
257         return hitCount;
258     }
259 
260     // == NG Search
261 
262     public IteratorSearchResponse searchIteratorPaged( IteratorSearchRequest request,
263                                                        Collection<IndexingContext> indexingContexts )
264         throws IOException
265     {
266         return searchIteratorPaged( request, indexingContexts, false );
267     }
268 
269     public IteratorSearchResponse forceSearchIteratorPaged( IteratorSearchRequest request,
270                                                             Collection<IndexingContext> indexingContexts )
271         throws IOException
272     {
273         return searchIteratorPaged( request, indexingContexts, true );
274     }
275 
276     private IteratorSearchResponse searchIteratorPaged( IteratorSearchRequest request,
277                                                         Collection<IndexingContext> indexingContexts,
278                                                         boolean ignoreContext )
279         throws IOException
280     {
281         List<IndexingContext> contexts = getParticipatingContexts( indexingContexts, ignoreContext );
282 
283         NexusIndexMultiReader multiReader = getMergedIndexReader( indexingContexts, ignoreContext );
284 
285         NexusIndexMultiSearcher indexSearcher = new NexusIndexMultiSearcher( multiReader );
286 
287         try
288         {
289             TopScoreDocCollector hits = doSearchWithCeiling( request, indexSearcher, request.getQuery() );
290 
291             return new IteratorSearchResponse( request.getQuery(), hits.getTotalHits(),
292                                                new DefaultIteratorResultSet( request, indexSearcher, contexts,
293                                                                              hits.topDocs() ) );
294         }
295         catch ( IOException | RuntimeException e )
296         {
297             try
298             {
299                 indexSearcher.release();
300             }
301             catch ( Exception secondary )
302             {
303                 // do not mask original exception
304             }
305             throw e;
306         }
307     }
308 
309     // ==
310 
311     protected TopScoreDocCollector doSearchWithCeiling( final AbstractSearchRequest request,
312                                                         final IndexSearcher indexSearcher, final Query query )
313         throws IOException
314     {
315         int topHitCount = getTopDocsCollectorHitNum( request, AbstractSearchRequest.UNDEFINED );
316 
317         if ( AbstractSearchRequest.UNDEFINED != topHitCount )
318         {
319             // count is set, simply just execute it as-is
320             final TopScoreDocCollector hits = TopScoreDocCollector.create( topHitCount, Integer.MAX_VALUE );
321 
322             indexSearcher.search( query, hits );
323 
324             return hits;
325         }
326         else
327         {
328             // set something reasonable as 1k
329             topHitCount = 1000;
330 
331             // perform search
332             TopScoreDocCollector hits = TopScoreDocCollector.create( topHitCount, Integer.MAX_VALUE );
333             indexSearcher.search( query, hits );
334 
335             // check total hits against, does it fit?
336             if ( topHitCount < hits.getTotalHits() )
337             {
338                 topHitCount = hits.getTotalHits();
339 
340                 if ( getLogger().isDebugEnabled() )
341                 {
342                     // warn the user and leave trace just before OOM might happen
343                     // the hits.getTotalHits() might be HUUGE
344                     getLogger().debug(
345                         "Executing unbounded search, and fitting topHitCounts to " + topHitCount
346                         + ", an OOMEx might follow. To avoid OOM use narrower queries or limit your expectancy with "
347                         + "request.setCount() method where appropriate. See MINDEXER-14 for details." );
348                 }
349 
350                 // redo all, but this time with correct numbers
351                 hits = TopScoreDocCollector.create( topHitCount, Integer.MAX_VALUE );
352                 indexSearcher.search( query, hits );
353             }
354 
355             return hits;
356         }
357     }
358 
359     /**
360      * Returns the list of participating contexts. Does not locks them, just builds a list of them.
361      */
362     protected List<IndexingContext> getParticipatingContexts( final Collection<IndexingContext> indexingContexts,
363                                                               final boolean ignoreContext )
364     {
365         // to not change the API all away, but we need stable ordering here
366         // filter for those 1st, that take part in here
367         final ArrayList<IndexingContext> contexts = new ArrayList<>( indexingContexts.size() );
368 
369         for ( IndexingContext ctx : indexingContexts )
370         {
371             if ( ignoreContext || ctx.isSearchable() )
372             {
373                 contexts.add( ctx );
374             }
375         }
376 
377         return contexts;
378     }
379 
380     /**
381      * Locks down participating contexts, and returns a "merged" reader of them. In case of error, unlocks as part of
382      * cleanup and re-throws exception. Without error, it is the duty of caller to unlock contexts!
383      * 
384      * @param indexingContexts
385      * @param ignoreContext
386      * @return
387      * @throws IOException
388      */
389     protected NexusIndexMultiReader getMergedIndexReader( final Collection<IndexingContext> indexingContexts,
390                                                           final boolean ignoreContext )
391         throws IOException
392     {
393         final List<IndexingContext> contexts = getParticipatingContexts( indexingContexts, ignoreContext );
394         return new NexusIndexMultiReader( contexts );
395     }
396 
397     protected int getTopDocsCollectorHitNum( final AbstractSearchRequest request, final int ceiling )
398     {
399         if ( request instanceof AbstractSearchPageableRequest )
400         {
401             final AbstractSearchPageableRequest prequest = (AbstractSearchPageableRequest) request;
402 
403             if ( AbstractSearchRequest.UNDEFINED != prequest.getCount() )
404             {
405                 // easy, user knows and tells us how many results he want
406                 return prequest.getCount() + prequest.getStart();
407             }
408         }
409         else
410         {
411             if ( AbstractSearchRequest.UNDEFINED != request.getCount() )
412             {
413                 // easy, user knows and tells us how many results he want
414                 return request.getCount();
415             }
416         }
417 
418         return ceiling;
419     }
420 }