|
46 | 46 | import org.apache.lucene.index.DirectoryReader;
|
47 | 47 | import org.apache.lucene.index.IndexReader;
|
48 | 48 | import org.apache.lucene.index.IndexableField;
|
| 49 | +import org.apache.lucene.index.LeafReaderContext; |
| 50 | +import org.apache.lucene.index.ReaderUtil; |
49 | 51 | import org.apache.lucene.index.Term;
|
50 | 52 | import org.apache.lucene.queryparser.classic.ParseException;
|
51 | 53 | import org.apache.lucene.search.IndexSearcher;
|
| 54 | +import org.apache.lucene.search.Matches; |
| 55 | +import org.apache.lucene.search.MatchesIterator; |
| 56 | +import org.apache.lucene.search.MatchesUtils; |
52 | 57 | import org.apache.lucene.search.Query;
|
53 | 58 | import org.apache.lucene.search.ScoreDoc;
|
| 59 | +import org.apache.lucene.search.ScoreMode; |
54 | 60 | import org.apache.lucene.search.Sort;
|
55 | 61 | import org.apache.lucene.search.SortField;
|
56 | 62 | import org.apache.lucene.search.TermQuery;
|
57 | 63 | import org.apache.lucene.search.TopDocs;
|
58 | 64 | import org.apache.lucene.search.TopFieldDocs;
|
| 65 | +import org.apache.lucene.search.Weight; |
59 | 66 | import org.apache.lucene.search.spell.DirectSpellChecker;
|
60 | 67 | import org.apache.lucene.search.spell.SuggestMode;
|
61 | 68 | import org.apache.lucene.search.spell.SuggestWord;
|
62 | 69 | import org.apache.lucene.store.FSDirectory;
|
| 70 | +import org.opengrok.indexer.analysis.AbstractAnalyzer; |
63 | 71 | import org.opengrok.indexer.analysis.AnalyzerGuru;
|
64 | 72 | import org.opengrok.indexer.analysis.CompatibleAnalyser;
|
65 | 73 | import org.opengrok.indexer.analysis.Definitions;
|
@@ -142,6 +150,11 @@ public class SearchHelper {
|
142 | 150 | * met.
|
143 | 151 | */
|
144 | 152 | public boolean isCrossRefSearch;
|
| 153 | + /** |
| 154 | + * As with {@link #isCrossRefSearch}, but here indicating either a |
| 155 | + * cross-reference search or a "full blown search". |
| 156 | + */ |
| 157 | + public boolean isGuiSearch; |
145 | 158 | /**
|
146 | 159 | * if not {@code null}, the consumer should redirect the client to a
|
147 | 160 | * separate result page denoted by the value of this field. Automatically
|
@@ -391,42 +404,93 @@ public SearchHelper executeQuery() {
|
391 | 404 | TopFieldDocs fdocs = searcher.search(query, start + maxItems, sort);
|
392 | 405 | totalHits = fdocs.totalHits.value;
|
393 | 406 | hits = fdocs.scoreDocs;
|
394 |
| - // Bug #3900: Check if this is a search for a single term, and that |
395 |
| - // term is a definition. If that's the case, and we only have one match, |
396 |
| - // we'll generate a direct link instead of a listing. |
397 |
| - boolean isSingleDefinitionSearch |
398 |
| - = (query instanceof TermQuery) && (builder.getDefs() != null); |
399 |
| - |
400 |
| - // Attempt to create a direct link to the definition if we search for |
401 |
| - // one single definition term AND we have exactly one match AND there |
402 |
| - // is only one definition of that symbol in the document that matches. |
403 |
| - boolean uniqueDefinition = false; |
404 |
| - Document doc = null; |
405 |
| - String symbol = null; |
406 |
| - if (isCrossRefSearch && isSingleDefinitionSearch && hits != null && hits.length == 1) { |
407 |
| - doc = searcher.doc(hits[0].doc); |
408 |
| - IndexableField tagsField = doc.getField(QueryBuilder.TAGS); |
409 |
| - if (tagsField != null) { |
410 |
| - byte[] rawTags = tagsField.binaryValue().bytes; |
411 |
| - Definitions tags = Definitions.deserialize(rawTags); |
412 |
| - symbol = ((TermQuery) query).getTerm().text(); |
413 |
| - if (tags.occurrences(symbol) == 1) { |
414 |
| - uniqueDefinition = true; |
415 |
| - } |
| 407 | + |
| 408 | + /* |
| 409 | + * Determine if possibly a single-result redirect to xref is |
| 410 | + * eligible and applicable. If history query is active, then nope. |
| 411 | + */ |
| 412 | + if (hits != null && hits.length == 1 && builder.getHist() == null) { |
| 413 | + int docID = hits[0].doc; |
| 414 | + if (isCrossRefSearch && query instanceof TermQuery && builder.getDefs() != null) { |
| 415 | + maybeRedirectToDefinition(docID, (TermQuery) query); |
| 416 | + } else if (isGuiSearch) { |
| 417 | + maybeRedirectToMatchOffset(docID, builder.getContextFields()); |
416 | 418 | }
|
417 | 419 | }
|
418 |
| - if (uniqueDefinition) { |
| 420 | + } catch (IOException | ClassNotFoundException e) { |
| 421 | + errorMsg = e.getMessage(); |
| 422 | + } |
| 423 | + return this; |
| 424 | + } |
| 425 | + |
| 426 | + private void maybeRedirectToDefinition(int docID, TermQuery termQuery) |
| 427 | + throws IOException, ClassNotFoundException { |
| 428 | + // Bug #3900: Check if this is a search for a single term, and that |
| 429 | + // term is a definition. If that's the case, and we only have one match, |
| 430 | + // we'll generate a direct link instead of a listing. |
| 431 | + // |
| 432 | + // Attempt to create a direct link to the definition if we search for |
| 433 | + // one single definition term AND we have exactly one match AND there |
| 434 | + // is only one definition of that symbol in the document that matches. |
| 435 | + Document doc = searcher.doc(docID); |
| 436 | + IndexableField tagsField = doc.getField(QueryBuilder.TAGS); |
| 437 | + if (tagsField != null) { |
| 438 | + byte[] rawTags = tagsField.binaryValue().bytes; |
| 439 | + Definitions tags = Definitions.deserialize(rawTags); |
| 440 | + String symbol = termQuery.getTerm().text(); |
| 441 | + if (tags.occurrences(symbol) == 1) { |
419 | 442 | String anchor = Util.URIEncode(symbol);
|
420 | 443 | redirect = contextPath + Prefix.XREF_P
|
421 | 444 | + Util.URIEncodePath(doc.get(QueryBuilder.PATH))
|
422 | 445 | + '?' + QueryParameters.FRAGMENT_IDENTIFIER_PARAM_EQ + anchor
|
423 | 446 | + '#' + anchor;
|
424 | 447 | }
|
425 |
| - } catch (IOException | ClassNotFoundException e) { |
426 |
| - errorMsg = e.getMessage(); |
427 | 448 | }
|
428 |
| - return this; |
429 | 449 | }
|
| 450 | + |
| 451 | + private void maybeRedirectToMatchOffset(int docID, List<String> contextFields) |
| 452 | + throws IOException { |
| 453 | + /* |
| 454 | + * Only PLAIN files might redirect to a file offset, since an offset |
| 455 | + * must be subsequently converted to a line number and that is tractable |
| 456 | + * only from plain text. |
| 457 | + */ |
| 458 | + Document doc = searcher.doc(docID); |
| 459 | + String genre = doc.get(QueryBuilder.T); |
| 460 | + if (!AbstractAnalyzer.Genre.PLAIN.typeName().equals(genre)) { |
| 461 | + return; |
| 462 | + } |
| 463 | + |
| 464 | + List<LeafReaderContext> leaves = reader.leaves(); |
| 465 | + int subIndex = ReaderUtil.subIndex(docID, leaves); |
| 466 | + LeafReaderContext leaf = leaves.get(subIndex); |
| 467 | + |
| 468 | + Query rewritten = query.rewrite(reader); |
| 469 | + Weight weight = rewritten.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1); |
| 470 | + Matches matches = weight.matches(leaf, docID); |
| 471 | + if (matches != MatchesUtils.MATCH_WITH_NO_TERMS) { |
| 472 | + int matchCount = 0; |
| 473 | + int offset = -1; |
| 474 | + for (String field : contextFields) { |
| 475 | + MatchesIterator matchesIterator = matches.getMatches(field); |
| 476 | + while (matchesIterator.next()) { |
| 477 | + if (matchesIterator.startOffset() >= 0) { |
| 478 | + // Abort if there is more than a single match offset. |
| 479 | + if (++matchCount > 1) { |
| 480 | + return; |
| 481 | + } |
| 482 | + offset = matchesIterator.startOffset(); |
| 483 | + } |
| 484 | + } |
| 485 | + } |
| 486 | + if (offset >= 0) { |
| 487 | + redirect = contextPath + Prefix.XREF_P |
| 488 | + + Util.URIEncodePath(doc.get(QueryBuilder.PATH)) |
| 489 | + + '?' + QueryParameters.MATCH_OFFSET_PARAM_EQ + offset; |
| 490 | + } |
| 491 | + } |
| 492 | + } |
| 493 | + |
430 | 494 | private static final Pattern TABSPACE = Pattern.compile("[\t ]+");
|
431 | 495 |
|
432 | 496 | private void getSuggestion(Term term, IndexReader ir,
|
|
0 commit comments