001/* 002 * File : $Source$ 003 * Date : $Date$ 004 * Version: $Revision$ 005 * 006 * This library is part of OpenCms - 007 * the Open Source Content Management System 008 * 009 * Copyright (C) 2002 - 2009 Alkacon Software (http://www.alkacon.com) 010 * 011 * This library is free software; you can redistribute it and/or 012 * modify it under the terms of the GNU Lesser General Public 013 * License as published by the Free Software Foundation; either 014 * version 2.1 of the License, or (at your option) any later version. 015 * 016 * This library is distributed in the hope that it will be useful, 017 * but WITHOUT ANY WARRANTY; without even the implied warranty of 018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 019 * Lesser General Public License for more details. 020 * 021 * For further information about Alkacon Software, please see the 022 * company website: http://www.alkacon.com 023 * 024 * For further information about OpenCms, please see the 025 * project website: http://www.opencms.org 026 * 027 * You should have received a copy of the GNU Lesser General Public 028 * License along with this library; if not, write to the Free Software 029 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 030 */ 031 032package org.opencms.search; 033 034import org.opencms.configuration.CmsParameterConfiguration; 035import org.opencms.file.CmsObject; 036import org.opencms.file.CmsResource; 037import org.opencms.file.CmsResourceFilter; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsIllegalArgumentException; 040import org.opencms.main.CmsLog; 041import org.opencms.main.OpenCms; 042import org.opencms.report.I_CmsReport; 043import org.opencms.search.documents.I_CmsTermHighlighter; 044import org.opencms.search.extractors.CmsExtractionResult; 045import org.opencms.search.extractors.I_CmsExtractionResult; 046import org.opencms.search.fields.CmsLuceneFieldConfiguration; 047import org.opencms.search.fields.CmsSearchField; 048import org.opencms.search.fields.CmsSearchFieldConfiguration; 049import org.opencms.util.CmsFileUtil; 050import org.opencms.util.CmsStringUtil; 051 052import java.io.File; 053import java.io.IOException; 054import java.nio.file.Paths; 055import java.text.ParseException; 056import java.util.ArrayList; 057import java.util.Calendar; 058import java.util.Collections; 059import java.util.Date; 060import java.util.HashMap; 061import java.util.List; 062import java.util.Locale; 063import java.util.Map; 064import java.util.Set; 065 066import org.apache.commons.logging.Log; 067import org.apache.lucene.analysis.Analyzer; 068import org.apache.lucene.document.DateTools; 069import org.apache.lucene.document.Document; 070import org.apache.lucene.index.DirectoryReader; 071import org.apache.lucene.index.FieldInfo; 072import org.apache.lucene.index.IndexReader; 073import org.apache.lucene.index.IndexWriter; 074import org.apache.lucene.index.IndexWriterConfig; 075import org.apache.lucene.index.StoredFieldVisitor; 076import org.apache.lucene.index.Term; 077import org.apache.lucene.queryparser.classic.QueryParser; 078import org.apache.lucene.search.BooleanClause; 079import org.apache.lucene.search.BooleanClause.Occur; 080import org.apache.lucene.search.BooleanQuery; 081import org.apache.lucene.search.IndexSearcher; 082import org.apache.lucene.search.MatchAllDocsQuery; 083import org.apache.lucene.search.MultiTermQuery; 084import org.apache.lucene.search.Query; 085import org.apache.lucene.search.ScoreMode; 086import org.apache.lucene.search.Sort; 087import org.apache.lucene.search.SortField; 088import org.apache.lucene.search.TermQuery; 089import org.apache.lucene.search.TopDocs; 090import org.apache.lucene.search.similarities.Similarity; 091import org.apache.lucene.store.Directory; 092import org.apache.lucene.store.FSDirectory; 093import org.apache.lucene.store.IOContext; 094import org.apache.solr.uninverting.UninvertingReader; 095import org.apache.solr.uninverting.UninvertingReader.Type; 096 097/** 098 * Abstract search index implementation.<p> 099 */ 100public class CmsSearchIndex extends A_CmsSearchIndex { 101 102 /** A constant for the full qualified name of the CmsSearchIndex class. */ 103 public static final String A_PARAM_PREFIX = "org.opencms.search.CmsSearchIndex"; 104 105 /** Constant for additional parameter to enable optimized full index regeneration (default: false). */ 106 public static final String BACKUP_REINDEXING = A_PARAM_PREFIX + ".useBackupReindexing"; 107 108 /** Look table to quickly zero-pad days / months in date Strings. */ 109 public static final String[] DATES = new String[] { 110 "00", 111 "01", 112 "02", 113 "03", 114 "04", 115 "05", 116 "06", 117 "07", 118 "08", 119 "09", 120 "10", 121 "11", 122 "12", 123 "13", 124 "14", 125 "15", 126 "16", 127 "17", 128 "18", 129 "19", 130 "20", 131 "21", 132 "22", 133 "23", 134 "24", 135 "25", 136 "26", 137 "27", 138 "28", 139 "29", 140 "30", 141 "31"}; 142 143 /** Constant for a field list that contains the "meta" field as well as the "content" field. */ 144 public static final String[] DOC_META_FIELDS = new String[] { 145 CmsSearchField.FIELD_META, 146 CmsSearchField.FIELD_CONTENT}; 147 148 /** Constant for additional parameter to enable excerpt creation (default: true). */ 149 public static final String EXCERPT = A_PARAM_PREFIX + ".createExcerpt"; 150 151 /** Constant for additional parameter for index content extraction. */ 152 public static final String EXTRACT_CONTENT = A_PARAM_PREFIX + ".extractContent"; 153 154 /** Constant for additional parameter to enable/disable language detection (default: false). */ 155 public static final String IGNORE_EXPIRATION = A_PARAM_PREFIX + ".ignoreExpiration"; 156 157 /** Constant for additional parameter to enable/disable language detection (default: false). */ 158 public static final String LANGUAGEDETECTION = "search.solr.useLanguageDetection"; 159 160 /** Constant for additional parameter for the Lucene index setting. */ 161 public static final String LUCENE_AUTO_COMMIT = "lucene.AutoCommit"; 162 163 /** Constant for additional parameter for the Lucene index setting. */ 164 public static final String LUCENE_RAM_BUFFER_SIZE_MB = "lucene.RAMBufferSizeMB"; 165 166 /** Constant for additional parameter for controlling how many hits are loaded at maximum (default: 1000). */ 167 public static final String MAX_HITS = A_PARAM_PREFIX + ".maxHits"; 168 169 /** Indicates how many hits are loaded at maximum by default. */ 170 public static final int MAX_HITS_DEFAULT = 5000; 171 172 /** Constant for years max range span in document search. */ 173 public static final int MAX_YEAR_RANGE = 25; 174 175 /** Constant for additional parameter to enable permission checks (default: true). */ 176 public static final String PERMISSIONS = A_PARAM_PREFIX + ".checkPermissions"; 177 178 /** Constant for additional parameter to set the thread priority during search. */ 179 public static final String PRIORITY = A_PARAM_PREFIX + ".priority"; 180 181 /** Constant for additional parameter to enable time range checks (default: true). */ 182 public static final String TIME_RANGE = A_PARAM_PREFIX + ".checkTimeRange"; 183 184 /** 185 * A stored field visitor, that does not return the large fields: "content" and "contentblob".<p> 186 */ 187 protected static final StoredFieldVisitor VISITOR = new StoredFieldVisitor() { 188 189 /** 190 * @see org.apache.lucene.index.StoredFieldVisitor#needsField(org.apache.lucene.index.FieldInfo) 191 */ 192 @Override 193 public Status needsField(FieldInfo fieldInfo) { 194 195 return !CmsSearchFieldConfiguration.LAZY_FIELDS.contains(fieldInfo.name) ? Status.YES : Status.NO; 196 } 197 }; 198 199 /** The log object for this class. */ 200 private static final Log LOG = CmsLog.getLog(CmsSearchIndex.class); 201 202 /** The serial version id. */ 203 private static final long serialVersionUID = 8461682478204452718L; 204 205 /** The configured Lucene analyzer used for this index. */ 206 private transient Analyzer m_analyzer; 207 208 /** Indicates if backup re-indexing is used by this index. */ 209 private boolean m_backupReindexing; 210 211 /** The permission check mode for this index. */ 212 private boolean m_checkPermissions; 213 214 /** The time range check mode for this index. */ 215 private boolean m_checkTimeRange; 216 217 /** The excerpt mode for this index. */ 218 private boolean m_createExcerpt; 219 220 /** Map of display query filters to use. */ 221 private transient Map<String, Query> m_displayFilters; 222 223 /** 224 * Signals whether expiration dates should be ignored when checking permissions or not.<p> 225 * @see #IGNORE_EXPIRATION 226 */ 227 private boolean m_ignoreExpiration; 228 229 /** The Lucene index searcher to use. */ 230 private transient IndexSearcher m_indexSearcher; 231 232 /** The Lucene index RAM buffer size, see {@link IndexWriterConfig#setRAMBufferSizeMB(double)}. */ 233 private Double m_luceneRAMBufferSizeMB; 234 235 /** Indicates how many hits are loaded at maximum. */ 236 private int m_maxHits; 237 238 /** The thread priority for a search. */ 239 private int m_priority; 240 241 /** Controls if a resource requires view permission to be displayed in the result list. */ 242 private boolean m_requireViewPermission; 243 244 /** The cms specific Similarity implementation. */ 245 private final transient Similarity m_sim = new CmsSearchSimilarity(); 246 247 /** 248 * Default constructor only intended to be used by the XML configuration. <p> 249 * 250 * It is recommended to use the constructor <code>{@link #CmsSearchIndex(String)}</code> 251 * as it enforces the mandatory name argument. <p> 252 */ 253 public CmsSearchIndex() { 254 255 super(); 256 m_checkPermissions = true; 257 m_priority = -1; 258 m_createExcerpt = true; 259 m_maxHits = MAX_HITS_DEFAULT; 260 m_checkTimeRange = false; 261 } 262 263 /** 264 * Creates a new CmsSearchIndex with the given name.<p> 265 * 266 * @param name the system-wide unique name for the search index 267 * 268 * @throws CmsIllegalArgumentException if the given name is null, empty or already taken by another search index 269 */ 270 public CmsSearchIndex(String name) 271 throws CmsIllegalArgumentException { 272 273 this(); 274 setName(name); 275 } 276 277 /** 278 * Generates a list of date terms for the optimized date range search with "daily" granularity level.<p> 279 * 280 * How this works:<ul> 281 * <li>For each document, terms are added for the year, the month and the day the document 282 * was modified or created) in. So for example if a document is modified at February 02, 2009, 283 * then the following terms are stored for this document: 284 * "20090202", "200902" and "2009".</li> 285 * <li>In case a date range search is done, then all possible matches for the 286 * provided rage are created as search terms and matched with the document terms.</li> 287 * <li>Consider the following use case: You want to find out if a resource has been changed 288 * in the time between November 29, 2007 and March 01, 2009. 289 * One term to match is simply "2008" because if a document 290 * was modified in 2008, then it is clearly in the date range. 291 * Other terms are "200712", "200901" and "200902", because all documents 292 * modified in these months are also a certain matches. 293 * Finally we need to add terms for "20071129", "20071130" and "20090301" to match the days in the 294 * starting and final month.</li> 295 * </ul> 296 * 297 * @param startDate start date of the range to search in 298 * @param endDate end date of the range to search in 299 * 300 * @return a list of date terms for the optimized date range search 301 */ 302 public static List<String> getDateRangeSpan(long startDate, long endDate) { 303 304 if (startDate > endDate) { 305 // switch so that the end is always before the start 306 long temp = endDate; 307 endDate = startDate; 308 startDate = temp; 309 } 310 311 List<String> result = new ArrayList<String>(100); 312 313 // initialize calendars from the time value 314 Calendar calStart = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone()); 315 Calendar calEnd = Calendar.getInstance(calStart.getTimeZone()); 316 calStart.setTimeInMillis(startDate); 317 calEnd.setTimeInMillis(endDate); 318 319 // get the required info to build the date range from the calendars 320 int startDay = calStart.get(Calendar.DAY_OF_MONTH); 321 int endDay = calEnd.get(Calendar.DAY_OF_MONTH); 322 int maxDayInStartMonth = calStart.getActualMaximum(Calendar.DAY_OF_MONTH); 323 int startMonth = calStart.get(Calendar.MONTH) + 1; 324 int endMonth = calEnd.get(Calendar.MONTH) + 1; 325 int startYear = calStart.get(Calendar.YEAR); 326 int endYear = calEnd.get(Calendar.YEAR); 327 328 // first add all full years in the date range 329 result.addAll(getYearSpan(startYear + 1, endYear - 1)); 330 331 if (startYear != endYear) { 332 // different year, different month 333 result.addAll(getMonthSpan(startMonth + 1, 12, startYear)); 334 result.addAll(getMonthSpan(1, endMonth - 1, endYear)); 335 result.addAll(getDaySpan(startDay, maxDayInStartMonth, startMonth, startYear)); 336 result.addAll(getDaySpan(1, endDay, endMonth, endYear)); 337 } else { 338 if (startMonth != endMonth) { 339 // same year, different month 340 result.addAll(getMonthSpan(startMonth + 1, endMonth - 1, startYear)); 341 result.addAll(getDaySpan(startDay, maxDayInStartMonth, startMonth, startYear)); 342 result.addAll(getDaySpan(1, endDay, endMonth, endYear)); 343 } else { 344 // same year, same month 345 result.addAll(getDaySpan(startDay, endDay, endMonth, endYear)); 346 } 347 } 348 349 // sort the result, makes the range better readable in the debugger 350 Collections.sort(result); 351 return result; 352 } 353 354 /** 355 * Calculate a span of days in the given year and month for the optimized date range search.<p> 356 * 357 * The result will contain dates formatted like "yyyyMMDD", for example "20080131".<p> 358 * 359 * @param startDay the start day 360 * @param endDay the end day 361 * @param month the month 362 * @param year the year 363 * 364 * @return a span of days in the given year and month for the optimized date range search 365 */ 366 private static List<String> getDaySpan(int startDay, int endDay, int month, int year) { 367 368 List<String> result = new ArrayList<String>(); 369 String yearMonthStr = String.valueOf(year) + DATES[month]; 370 for (int i = startDay; i <= endDay; i++) { 371 String dateStr = yearMonthStr + DATES[i]; 372 result.add(dateStr); 373 } 374 return result; 375 } 376 377 /** 378 * Calculate a span of months in the given year for the optimized date range search.<p> 379 * 380 * The result will contain dates formatted like "yyyyMM", for example "200801".<p> 381 * 382 * @param startMonth the start month 383 * @param endMonth the end month 384 * @param year the year 385 * 386 * @return a span of months in the given year for the optimized date range search 387 */ 388 private static List<String> getMonthSpan(int startMonth, int endMonth, int year) { 389 390 List<String> result = new ArrayList<String>(); 391 String yearStr = String.valueOf(year); 392 for (int i = startMonth; i <= endMonth; i++) { 393 String dateStr = yearStr + DATES[i]; 394 result.add(dateStr); 395 } 396 return result; 397 } 398 399 /** 400 * Calculate a span of years for the optimized date range search.<p> 401 * 402 * The result will contain dates formatted like "yyyy", for example "2008".<p> 403 * 404 * @param startYear the start year 405 * @param endYear the end year 406 * 407 * @return a span of years for the optimized date range search 408 */ 409 private static List<String> getYearSpan(int startYear, int endYear) { 410 411 List<String> result = new ArrayList<String>(); 412 for (int i = startYear; i <= endYear; i++) { 413 String dateStr = String.valueOf(i); 414 result.add(dateStr); 415 } 416 return result; 417 } 418 419 /** 420 * Adds a parameter.<p> 421 * 422 * @param key the key/name of the parameter 423 * @param value the value of the parameter 424 * 425 */ 426 @Override 427 public void addConfigurationParameter(String key, String value) { 428 429 if (PERMISSIONS.equals(key)) { 430 m_checkPermissions = Boolean.valueOf(value).booleanValue(); 431 } else if (EXTRACT_CONTENT.equals(key)) { 432 setExtractContent(Boolean.valueOf(value).booleanValue()); 433 } else if (BACKUP_REINDEXING.equals(key)) { 434 m_backupReindexing = Boolean.valueOf(value).booleanValue(); 435 } else if (LANGUAGEDETECTION.equals(key)) { 436 setLanguageDetection(Boolean.valueOf(value).booleanValue()); 437 } else if (IGNORE_EXPIRATION.equals(key)) { 438 m_ignoreExpiration = Boolean.valueOf(value).booleanValue(); 439 } else if (PRIORITY.equals(key)) { 440 m_priority = Integer.parseInt(value); 441 if (m_priority < Thread.MIN_PRIORITY) { 442 m_priority = Thread.MIN_PRIORITY; 443 LOG.error( 444 Messages.get().getBundle().key( 445 Messages.LOG_SEARCH_PRIORITY_TOO_LOW_2, 446 value, 447 Integer.valueOf(Thread.MIN_PRIORITY))); 448 449 } else if (m_priority > Thread.MAX_PRIORITY) { 450 m_priority = Thread.MAX_PRIORITY; 451 LOG.debug( 452 Messages.get().getBundle().key( 453 Messages.LOG_SEARCH_PRIORITY_TOO_HIGH_2, 454 value, 455 Integer.valueOf(Thread.MAX_PRIORITY))); 456 } 457 } 458 459 if (MAX_HITS.equals(key)) { 460 try { 461 m_maxHits = Integer.parseInt(value); 462 } catch (NumberFormatException e) { 463 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PARAM_3, value, key, getName())); 464 } 465 if (m_maxHits < (MAX_HITS_DEFAULT / 100)) { 466 m_maxHits = MAX_HITS_DEFAULT; 467 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PARAM_3, value, key, getName())); 468 } 469 } else if (TIME_RANGE.equals(key)) { 470 m_checkTimeRange = Boolean.valueOf(value).booleanValue(); 471 } else if (CmsSearchIndex.EXCERPT.equals(key)) { 472 m_createExcerpt = Boolean.valueOf(value).booleanValue(); 473 474 } else if (LUCENE_RAM_BUFFER_SIZE_MB.equals(key)) { 475 try { 476 m_luceneRAMBufferSizeMB = Double.valueOf(value); 477 } catch (NumberFormatException e) { 478 LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PARAM_3, value, key, getName())); 479 } 480 } 481 } 482 483 /** 484 * Creates an empty document that can be used by this search field configuration.<p> 485 * 486 * @param resource the resource to create the document for 487 * 488 * @return a new and empty document 489 */ 490 public I_CmsSearchDocument createEmptyDocument(CmsResource resource) { 491 492 return new CmsLuceneDocument(new Document()); 493 } 494 495 /** 496 * Returns the Lucene analyzer used for this index.<p> 497 * 498 * @return the Lucene analyzer used for this index 499 */ 500 public Analyzer getAnalyzer() { 501 502 return m_analyzer; 503 } 504 505 /** 506 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 507 */ 508 @Override 509 public CmsParameterConfiguration getConfiguration() { 510 511 CmsParameterConfiguration result = new CmsParameterConfiguration(); 512 if (getPriority() > 0) { 513 result.put(PRIORITY, String.valueOf(m_priority)); 514 } 515 if (!isExtractingContent()) { 516 result.put(EXTRACT_CONTENT, String.valueOf(isExtractingContent())); 517 } 518 if (!isCheckingPermissions()) { 519 result.put(PERMISSIONS, String.valueOf(m_checkPermissions)); 520 } 521 if (isBackupReindexing()) { 522 result.put(BACKUP_REINDEXING, String.valueOf(m_backupReindexing)); 523 } 524 if (isLanguageDetection()) { 525 result.put(LANGUAGEDETECTION, String.valueOf(isLanguageDetection())); 526 } 527 if (getMaxHits() != MAX_HITS_DEFAULT) { 528 result.put(MAX_HITS, String.valueOf(getMaxHits())); 529 } 530 if (!isCreatingExcerpt()) { 531 result.put(EXCERPT, String.valueOf(m_createExcerpt)); 532 } 533 if (m_luceneRAMBufferSizeMB != null) { 534 result.put(LUCENE_RAM_BUFFER_SIZE_MB, String.valueOf(m_luceneRAMBufferSizeMB)); 535 } 536 // always write time range check parameter because of logic change in OpenCms 8.0 537 result.put(TIME_RANGE, String.valueOf(m_checkTimeRange)); 538 return result; 539 } 540 541 /** 542 * @see org.opencms.search.I_CmsSearchIndex#getContentIfUnchanged(org.opencms.file.CmsResource) 543 */ 544 @Override 545 public I_CmsExtractionResult getContentIfUnchanged(CmsResource resource) { 546 547 // compare "date of last modification of content" from Lucene index and OpenCms VFS 548 // if this is identical, then the data from the Lucene index can be re-used 549 I_CmsSearchDocument oldDoc = getDocument(CmsSearchField.FIELD_PATH, resource.getRootPath()); 550 // first check if the document is already in the index 551 if ((oldDoc != null) && (oldDoc.getFieldValueAsDate(CmsSearchField.FIELD_DATE_CONTENT) != null)) { 552 long contentDateIndex = oldDoc.getFieldValueAsDate(CmsSearchField.FIELD_DATE_CONTENT).getTime(); 553 // now compare the date with the date stored in the resource 554 if (contentDateIndex == resource.getDateContent()) { 555 // extract stored content blob from index 556 return CmsExtractionResult.fromBytes(oldDoc.getContentBlob()); 557 } 558 } 559 return null; 560 } 561 562 /** 563 * Returns a document by document ID.<p> 564 * 565 * @param docId the id to get the document for 566 * 567 * @return the CMS specific document 568 */ 569 public I_CmsSearchDocument getDocument(int docId) { 570 571 try { 572 IndexSearcher searcher = getSearcher(); 573 return new CmsLuceneDocument(searcher.doc(docId)); 574 } catch (IOException e) { 575 // ignore, return null and assume document was not found 576 } 577 return null; 578 } 579 580 /** 581 * Returns the Lucene document with the given root path from the index.<p> 582 * 583 * @param rootPath the root path of the document to get 584 * 585 * @return the Lucene document with the given root path from the index 586 * 587 * @deprecated Use {@link #getDocument(String, String)} instead and provide {@link org.opencms.search.fields.CmsLuceneField#FIELD_PATH} as field to search in 588 */ 589 @Deprecated 590 public Document getDocument(String rootPath) { 591 592 if (getDocument(CmsSearchField.FIELD_PATH, rootPath) != null) { 593 return (Document)getDocument(CmsSearchField.FIELD_PATH, rootPath).getDocument(); 594 } 595 return null; 596 } 597 598 /** 599 * Returns the first document where the given term matches the selected index field.<p> 600 * 601 * Use this method to search for documents which have unique field values, like a unique id.<p> 602 * 603 * @param field the field to search in 604 * @param term the term to search for 605 * 606 * @return the first document where the given term matches the selected index field 607 */ 608 public I_CmsSearchDocument getDocument(String field, String term) { 609 610 Document result = null; 611 IndexSearcher searcher = getSearcher(); 612 if (searcher != null) { 613 // search for an exact match on the selected field 614 Term resultTerm = new Term(field, term); 615 try { 616 TopDocs hits = searcher.search(new TermQuery(resultTerm), 1); 617 if (hits.scoreDocs.length > 0) { 618 result = searcher.doc(hits.scoreDocs[0].doc); 619 } 620 } catch (IOException e) { 621 // ignore, return null and assume document was not found 622 } 623 } 624 if (result != null) { 625 return new CmsLuceneDocument(result); 626 } 627 return null; 628 } 629 630 /** 631 * Returns the language locale for the given resource in this index.<p> 632 * 633 * @param cms the current OpenCms user context 634 * @param resource the resource to check 635 * @param availableLocales a list of locales supported by the resource 636 * 637 * @return the language locale for the given resource in this index 638 */ 639 @Override 640 public Locale getLocaleForResource(CmsObject cms, CmsResource resource, List<Locale> availableLocales) { 641 642 Locale result; 643 List<Locale> defaultLocales = OpenCms.getLocaleManager().getDefaultLocales(cms, resource); 644 List<Locale> locales = availableLocales; 645 if ((locales == null) || (locales.size() == 0)) { 646 locales = defaultLocales; 647 } 648 result = OpenCms.getLocaleManager().getBestMatchingLocale(getLocale(), defaultLocales, locales); 649 return result; 650 } 651 652 /** 653 * Returns the language locale of the index as a String.<p> 654 * 655 * @return the language locale of the index as a String 656 * 657 * @see #getLocale() 658 */ 659 public String getLocaleString() { 660 661 return getLocale().toString(); 662 } 663 664 /** 665 * Indicates the number of how many hits are loaded at maximum.<p> 666 * 667 * The number of maximum documents to load from the index 668 * must be specified. The default of this setting is {@link CmsSearchIndex#MAX_HITS_DEFAULT} (5000). 669 * This means that at maximum 5000 results are returned from the index. 670 * Please note that this number may be reduced further because of OpenCms read permissions 671 * or per-user file visibility settings not controlled in the index.<p> 672 * 673 * @return the number of how many hits are loaded at maximum 674 * 675 * @since 7.5.1 676 */ 677 public int getMaxHits() { 678 679 return m_maxHits; 680 } 681 682 /** 683 * Returns the path where this index stores it's data in the "real" file system.<p> 684 * 685 * @return the path where this index stores it's data in the "real" file system 686 */ 687 @Override 688 public String getPath() { 689 690 if (super.getPath() == null) { 691 setPath(generateIndexDirectory()); 692 } 693 return super.getPath(); 694 } 695 696 /** 697 * Returns the Thread priority for this search index.<p> 698 * 699 * @return the Thread priority for this search index 700 */ 701 public int getPriority() { 702 703 return m_priority; 704 } 705 706 /** 707 * Returns the Lucene index searcher used for this search index.<p> 708 * 709 * @return the Lucene index searcher used for this search index 710 */ 711 public IndexSearcher getSearcher() { 712 713 return m_indexSearcher; 714 } 715 716 /** 717 * @see org.opencms.search.A_CmsSearchIndex#initialize() 718 */ 719 @Override 720 public void initialize() throws CmsSearchException { 721 722 super.initialize(); 723 724 // get the configured analyzer and apply the the field configuration analyzer wrapper 725 @SuppressWarnings("resource") 726 Analyzer baseAnalyzer = OpenCms.getSearchManager().getAnalyzer(getLocale()); 727 728 if (getFieldConfiguration() instanceof CmsLuceneFieldConfiguration) { 729 CmsLuceneFieldConfiguration fc = (CmsLuceneFieldConfiguration)getFieldConfiguration(); 730 setAnalyzer(fc.getAnalyzer(baseAnalyzer)); 731 } 732 } 733 734 /** 735 * Returns <code>true</code> if backup re-indexing is done by this index.<p> 736 * 737 * This is an optimization method by which the old extracted content is 738 * reused in order to save performance when re-indexing.<p> 739 * 740 * @return <code>true</code> if backup re-indexing is done by this index 741 * 742 * @since 7.5.1 743 */ 744 public boolean isBackupReindexing() { 745 746 return m_backupReindexing; 747 } 748 749 /** 750 * Returns <code>true</code> if permissions are checked for search results by this index.<p> 751 * 752 * If permission checks are not required, they can be turned off in the index search configuration parameters 753 * in <code>opencms-search.xml</code>. Not checking permissions will improve performance.<p> 754 * 755 * This is can be of use in scenarios when you know that all search results are always readable, 756 * which is usually true for public websites that do not have personalized accounts.<p> 757 * 758 * Please note that even if a result is returned where the current user has no read permissions, 759 * the user can not actually access this document. It will only appear in the search result list, 760 * but if the user clicks the link to open the document he will get an error.<p> 761 * 762 * 763 * @return <code>true</code> if permissions are checked for search results by this index 764 */ 765 public boolean isCheckingPermissions() { 766 767 return m_checkPermissions; 768 } 769 770 /** 771 * Returns <code>true</code> if the document time range is checked with a granularity level of seconds 772 * for search results by this index.<p> 773 * 774 * Since OpenCms 8.0, time range checks are always done if {@link CmsSearchParameters#setMinDateLastModified(long)} 775 * or any of the corresponding methods are used. 776 * This is done very efficiently using optimized Lucene filers. 777 * However, the granularity of these checks are done only on a daily 778 * basis, which means that you can only find "changes made yesterday" but not "changes made last hour". 779 * For normal limitation of search results, a daily granularity should be enough.<p> 780 * 781 * If time range checks with a granularity level of seconds are required, 782 * they can be turned on in the index search configuration parameters 783 * in <code>opencms-search.xml</code>. 784 * Not checking the time range with a granularity level of seconds will improve performance.<p> 785 * 786 * By default the granularity level of seconds is turned off since OpenCms 8.0<p> 787 * 788 * @return <code>true</code> if the document time range is checked with a granularity level of seconds for search results by this index 789 */ 790 public boolean isCheckingTimeRange() { 791 792 return m_checkTimeRange; 793 } 794 795 /** 796 * Returns the checkPermissions.<p> 797 * 798 * @return the checkPermissions 799 */ 800 public boolean isCheckPermissions() { 801 802 return m_checkPermissions; 803 } 804 805 /** 806 * Returns <code>true</code> if an excerpt is generated by this index.<p> 807 * 808 * If no except is required, generation can be turned off in the index search configuration parameters 809 * in <code>opencms-search.xml</code>. Not generating an excerpt will improve performance.<p> 810 * 811 * @return <code>true</code> if an excerpt is generated by this index 812 */ 813 public boolean isCreatingExcerpt() { 814 815 return m_createExcerpt; 816 } 817 818 /** 819 * Returns the ignoreExpiration.<p> 820 * 821 * @return the ignoreExpiration 822 */ 823 public boolean isIgnoreExpiration() { 824 825 return m_ignoreExpiration; 826 } 827 828 /** 829 * @see org.opencms.search.A_CmsSearchIndex#isInitialized() 830 */ 831 @Override 832 public boolean isInitialized() { 833 834 return super.isInitialized() && (null != getPath()); 835 } 836 837 /** 838 * Returns <code>true</code> if a resource requires read permission to be included in the result list.<p> 839 * 840 * @return <code>true</code> if a resource requires read permission to be included in the result list 841 */ 842 public boolean isRequireViewPermission() { 843 844 return m_requireViewPermission; 845 } 846 847 /** 848 * @see org.opencms.search.A_CmsSearchIndex#onIndexChanged(boolean) 849 */ 850 @Override 851 public void onIndexChanged(boolean force) { 852 853 if (force) { 854 indexSearcherOpen(getPath()); 855 } else { 856 indexSearcherUpdate(); 857 } 858 } 859 860 /** 861 * Performs a search on the index within the given fields.<p> 862 * 863 * The result is returned as List with entries of type I_CmsSearchResult.<p> 864 * 865 * @param cms the current user's Cms object 866 * @param params the parameters to use for the search 867 * 868 * @return the List of results found or an empty list 869 * 870 * @throws CmsSearchException if something goes wrong 871 */ 872 public CmsSearchResultList search(CmsObject cms, CmsSearchParameters params) throws CmsSearchException { 873 874 long timeTotal = -System.currentTimeMillis(); 875 long timeLucene; 876 long timeResultProcessing; 877 878 if (LOG.isDebugEnabled()) { 879 LOG.debug(Messages.get().getBundle().key(Messages.LOG_SEARCH_PARAMS_2, params, getName())); 880 } 881 882 // the hits found during the search 883 TopDocs hits; 884 885 // storage for the results found 886 CmsSearchResultList searchResults = new CmsSearchResultList(); 887 888 int previousPriority = Thread.currentThread().getPriority(); 889 890 try { 891 // copy the user OpenCms context 892 CmsObject searchCms = OpenCms.initCmsObject(cms); 893 894 if (getPriority() > 0) { 895 // change thread priority in order to reduce search impact on overall system performance 896 Thread.currentThread().setPriority(getPriority()); 897 } 898 899 // change the project 900 searchCms.getRequestContext().setCurrentProject(searchCms.readProject(getProject())); 901 902 timeLucene = -System.currentTimeMillis(); 903 904 // several search options are searched using filters 905 BooleanQuery.Builder builder = new BooleanQuery.Builder(); 906 // append root path filter 907 builder = appendPathFilter(searchCms, builder, params.getRoots()); 908 // append category filter 909 builder = appendCategoryFilter(searchCms, builder, params.getCategories()); 910 // append resource type filter 911 builder = appendResourceTypeFilter(searchCms, builder, params.getResourceTypes()); 912 913 // append date last modified filter 914 builder = appendDateLastModifiedFilter( 915 builder, 916 params.getMinDateLastModified(), 917 params.getMaxDateLastModified()); 918 // append date created filter 919 builder = appendDateCreatedFilter(builder, params.getMinDateCreated(), params.getMaxDateCreated()); 920 921 // the search query to use, will be constructed in the next lines 922 Query query = null; 923 // store separate fields query for excerpt highlighting 924 Query fieldsQuery = null; 925 926 // get an index searcher that is certainly up to date 927 indexSearcherUpdate(); 928 IndexSearcher searcher = getSearcher(); 929 930 if (!params.isIgnoreQuery()) { 931 // since OpenCms 8 the query can be empty in which case only filters are used for the result 932 if (params.getParsedQuery() != null) { 933 // the query was already build, re-use it 934 QueryParser p = new QueryParser(CmsSearchField.FIELD_CONTENT, getAnalyzer()); 935 fieldsQuery = p.parse(params.getParsedQuery()); 936 } else if (params.getFieldQueries() != null) { 937 // each field has an individual query 938 BooleanQuery.Builder mustOccur = null; 939 BooleanQuery.Builder shouldOccur = null; 940 for (CmsSearchParameters.CmsSearchFieldQuery fq : params.getFieldQueries()) { 941 // add one sub-query for each defined field 942 QueryParser p = new QueryParser(fq.getFieldName(), getAnalyzer()); 943 // first generate the combined keyword query 944 Query keywordQuery = null; 945 if (fq.getSearchTerms().size() == 1) { 946 // this is just a single size keyword list 947 keywordQuery = p.parse(fq.getSearchTerms().get(0)); 948 } else { 949 // multiple size keyword list 950 BooleanQuery.Builder keywordListQuery = new BooleanQuery.Builder(); 951 for (String keyword : fq.getSearchTerms()) { 952 keywordListQuery.add(p.parse(keyword), fq.getTermOccur()); 953 } 954 keywordQuery = keywordListQuery.build(); 955 } 956 if (BooleanClause.Occur.SHOULD.equals(fq.getOccur())) { 957 if (shouldOccur == null) { 958 shouldOccur = new BooleanQuery.Builder(); 959 } 960 shouldOccur.add(keywordQuery, fq.getOccur()); 961 } else { 962 if (mustOccur == null) { 963 mustOccur = new BooleanQuery.Builder(); 964 } 965 mustOccur.add(keywordQuery, fq.getOccur()); 966 } 967 } 968 BooleanQuery.Builder booleanFieldsQuery = new BooleanQuery.Builder(); 969 if (mustOccur != null) { 970 booleanFieldsQuery.add(mustOccur.build(), BooleanClause.Occur.MUST); 971 } 972 if (shouldOccur != null) { 973 booleanFieldsQuery.add(shouldOccur.build(), BooleanClause.Occur.MUST); 974 } 975 fieldsQuery = searcher.rewrite(booleanFieldsQuery.build()); 976 } else if ((params.getFields() != null) && (params.getFields().size() > 0)) { 977 // no individual field queries have been defined, so use one query for all fields 978 BooleanQuery.Builder booleanFieldsQuery = new BooleanQuery.Builder(); 979 // this is a "regular" query over one or more fields 980 // add one sub-query for each of the selected fields, e.g. "content", "title" etc. 981 for (int i = 0; i < params.getFields().size(); i++) { 982 QueryParser p = new QueryParser(params.getFields().get(i), getAnalyzer()); 983 p.setMultiTermRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_REWRITE); 984 booleanFieldsQuery.add(p.parse(params.getQuery()), BooleanClause.Occur.SHOULD); 985 } 986 fieldsQuery = searcher.rewrite(booleanFieldsQuery.build()); 987 } else { 988 // if no fields are provided, just use the "content" field by default 989 QueryParser p = new QueryParser(CmsSearchField.FIELD_CONTENT, getAnalyzer()); 990 fieldsQuery = searcher.rewrite(p.parse(params.getQuery())); 991 } 992 993 // finally set the main query to the fields query 994 // please note that we still need both variables in case the query is a MatchAllDocsQuery - see below 995 query = fieldsQuery; 996 } 997 998 if (LOG.isDebugEnabled()) { 999 LOG.debug(Messages.get().getBundle().key(Messages.LOG_BASE_QUERY_1, query)); 1000 } 1001 1002 if (query == null) { 1003 // if no text query is set, then we match all documents 1004 query = new MatchAllDocsQuery(); 1005 } else { 1006 // store the parsed query for page browsing 1007 params.setParsedQuery(query.toString(CmsSearchField.FIELD_CONTENT)); 1008 } 1009 1010 // build the final query 1011 final BooleanQuery.Builder finalQueryBuilder = new BooleanQuery.Builder(); 1012 finalQueryBuilder.add(query, BooleanClause.Occur.MUST); 1013 finalQueryBuilder.add(builder.build(), BooleanClause.Occur.FILTER); 1014 final BooleanQuery finalQuery = finalQueryBuilder.build(); 1015 1016 // collect the categories 1017 CmsSearchCategoryCollector categoryCollector; 1018 if (params.isCalculateCategories()) { 1019 // USE THIS OPTION WITH CAUTION 1020 // this may slow down searched by an order of magnitude 1021 categoryCollector = new CmsSearchCategoryCollector(searcher); 1022 // perform a first search to collect the categories 1023 searcher.search(finalQuery, categoryCollector); 1024 // store the result 1025 searchResults.setCategories(categoryCollector.getCategoryCountResult()); 1026 } 1027 1028 // get maxScore first, since Lucene 8, it's not computed automatically anymore 1029 TopDocs scoreHits = searcher.search(query, 1); 1030 float maxScore = scoreHits.scoreDocs.length == 0 ? Float.NaN : scoreHits.scoreDocs[0].score; 1031 // perform the search operation 1032 if ((params.getSort() == null) || (params.getSort() == CmsSearchParameters.SORT_DEFAULT)) { 1033 // apparently scoring is always enabled by Lucene if no sort order is provided 1034 hits = searcher.search(finalQuery, getMaxHits()); 1035 } else { 1036 // if a sort order is provided, we must check if scoring must be calculated by the searcher 1037 boolean isSortScore = isSortScoring(searcher, params.getSort()); 1038 hits = searcher.search(finalQuery, getMaxHits(), params.getSort(), isSortScore); 1039 } 1040 1041 timeLucene += System.currentTimeMillis(); 1042 timeResultProcessing = -System.currentTimeMillis(); 1043 1044 if (hits != null) { 1045 long hitCount = hits.totalHits.value > hits.scoreDocs.length 1046 ? hits.scoreDocs.length 1047 : hits.totalHits.value; 1048 int page = params.getSearchPage(); 1049 long start = -1, end = -1; 1050 if ((params.getMatchesPerPage() > 0) && (page > 0) && (hitCount > 0)) { 1051 // calculate the final size of the search result 1052 start = params.getMatchesPerPage() * (page - 1); 1053 end = start + params.getMatchesPerPage(); 1054 // ensure that both i and n are inside the range of foundDocuments.size() 1055 start = (start > hitCount) ? hitCount : start; 1056 end = (end > hitCount) ? hitCount : end; 1057 } else { 1058 // return all found documents in the search result 1059 start = 0; 1060 end = hitCount; 1061 } 1062 1063 Set<String> returnFields = ((CmsLuceneFieldConfiguration)getFieldConfiguration()).getReturnFields(); 1064 Set<String> excerptFields = ((CmsLuceneFieldConfiguration)getFieldConfiguration()).getExcerptFields(); 1065 1066 long visibleHitCount = hitCount; 1067 for (int i = 0, cnt = 0; (i < hitCount) && (cnt < end); i++) { 1068 try { 1069 Document doc = searcher.doc(hits.scoreDocs[i].doc, returnFields); 1070 I_CmsSearchDocument searchDoc = new CmsLuceneDocument(doc); 1071 searchDoc.setScore(hits.scoreDocs[i].score); 1072 if ((isInTimeRange(doc, params)) && (hasReadPermission(searchCms, searchDoc))) { 1073 // user has read permission 1074 if (cnt >= start) { 1075 // do not use the resource to obtain the raw content, read it from the lucene document! 1076 String excerpt = null; 1077 if (isCreatingExcerpt() && (fieldsQuery != null)) { 1078 Document exDoc = searcher.doc(hits.scoreDocs[i].doc, excerptFields); 1079 I_CmsTermHighlighter highlighter = OpenCms.getSearchManager().getHighlighter(); 1080 excerpt = highlighter.getExcerpt(exDoc, this, params, fieldsQuery, getAnalyzer()); 1081 } 1082 int score = Math.round( 1083 (maxScore != Float.NaN ? (hits.scoreDocs[i].score / maxScore) * 100f : 0)); 1084 searchResults.add(new CmsSearchResult(score, doc, excerpt)); 1085 } 1086 cnt++; 1087 } else { 1088 visibleHitCount--; 1089 } 1090 } catch (Exception e) { 1091 // should not happen, but if it does we want to go on with the next result nevertheless 1092 if (LOG.isWarnEnabled()) { 1093 LOG.warn(Messages.get().getBundle().key(Messages.LOG_RESULT_ITERATION_FAILED_0), e); 1094 } 1095 } 1096 } 1097 1098 // save the total count of search results 1099 searchResults.setHitCount((int)visibleHitCount); 1100 } else { 1101 searchResults.setHitCount(0); 1102 } 1103 1104 timeResultProcessing += System.currentTimeMillis(); 1105 } catch (RuntimeException e) { 1106 throw new CmsSearchException(Messages.get().container(Messages.ERR_SEARCH_PARAMS_1, params), e); 1107 } catch (Exception e) { 1108 throw new CmsSearchException(Messages.get().container(Messages.ERR_SEARCH_PARAMS_1, params), e); 1109 } finally { 1110 1111 // re-set thread to previous priority 1112 Thread.currentThread().setPriority(previousPriority); 1113 } 1114 1115 if (LOG.isDebugEnabled()) { 1116 timeTotal += System.currentTimeMillis(); 1117 Object[] logParams = new Object[] { 1118 Long.valueOf(hits == null ? 0 : hits.totalHits.value), 1119 Long.valueOf(timeTotal), 1120 Long.valueOf(timeLucene), 1121 Long.valueOf(timeResultProcessing)}; 1122 LOG.debug(Messages.get().getBundle().key(Messages.LOG_STAT_RESULTS_TIME_4, logParams)); 1123 } 1124 1125 return searchResults; 1126 } 1127 1128 /** 1129 * Sets the Lucene analyzer used for this index.<p> 1130 * 1131 * @param analyzer the Lucene analyzer to set 1132 */ 1133 public void setAnalyzer(Analyzer analyzer) { 1134 1135 m_analyzer = analyzer; 1136 } 1137 1138 /** 1139 * Sets the checkPermissions.<p> 1140 * 1141 * @param checkPermissions the checkPermissions to set 1142 */ 1143 public void setCheckPermissions(boolean checkPermissions) { 1144 1145 m_checkPermissions = checkPermissions; 1146 } 1147 1148 /** 1149 * Sets the ignoreExpiration.<p> 1150 * 1151 * @param ignoreExpiration the ignoreExpiration to set 1152 */ 1153 public void setIgnoreExpiration(boolean ignoreExpiration) { 1154 1155 m_ignoreExpiration = ignoreExpiration; 1156 } 1157 1158 /** 1159 * Sets the number of how many hits are loaded at maximum.<p> 1160 * 1161 * This must be set at least to 50, or this setting is ignored.<p> 1162 * 1163 * @param maxHits the number of how many hits are loaded at maximum to set 1164 * 1165 * @see #getMaxHits() 1166 * 1167 * @since 7.5.1 1168 */ 1169 public void setMaxHits(int maxHits) { 1170 1171 if (m_maxHits >= (MAX_HITS_DEFAULT / 100)) { 1172 m_maxHits = maxHits; 1173 } 1174 } 1175 1176 /** 1177 * Controls if a resource requires view permission to be displayed in the result list.<p> 1178 * 1179 * By default this is <code>false</code>.<p> 1180 * 1181 * @param requireViewPermission controls if a resource requires view permission to be displayed in the result list 1182 */ 1183 public void setRequireViewPermission(boolean requireViewPermission) { 1184 1185 m_requireViewPermission = requireViewPermission; 1186 } 1187 1188 /** 1189 * Shuts down the search index.<p> 1190 * 1191 * This will close the local Lucene index searcher instance.<p> 1192 */ 1193 @Override 1194 public void shutDown() { 1195 1196 super.shutDown(); 1197 indexSearcherClose(); 1198 if (m_analyzer != null) { 1199 m_analyzer.close(); 1200 } 1201 if (CmsLog.INIT.isInfoEnabled()) { 1202 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_INDEX_1, getName())); 1203 } 1204 } 1205 1206 /** 1207 * Returns the name (<code>{@link #getName()}</code>) of this search index.<p> 1208 * 1209 * @return the name (<code>{@link #getName()}</code>) of this search index 1210 * 1211 * @see java.lang.Object#toString() 1212 */ 1213 @Override 1214 public String toString() { 1215 1216 return getName(); 1217 } 1218 1219 /** 1220 * Appends the a category filter to the given filter clause that matches all given categories.<p> 1221 * 1222 * In case the provided List is null or empty, the original filter is left unchanged.<p> 1223 * 1224 * The original filter parameter is extended and also provided as return value.<p> 1225 * 1226 * @param cms the current OpenCms search context 1227 * @param filter the filter to extend 1228 * @param categories the categories that will compose the filter 1229 * 1230 * @return the extended filter clause 1231 */ 1232 protected BooleanQuery.Builder appendCategoryFilter( 1233 CmsObject cms, 1234 BooleanQuery.Builder filter, 1235 List<String> categories) { 1236 1237 if ((categories != null) && (categories.size() > 0)) { 1238 // add query categories (if required) 1239 1240 // categories are indexed as lower-case strings 1241 // @see org.opencms.search.fields.CmsSearchFieldConfiguration#appendCategories 1242 List<String> lowerCaseCategories = new ArrayList<String>(); 1243 for (String category : categories) { 1244 lowerCaseCategories.add(category.toLowerCase()); 1245 } 1246 filter.add( 1247 new BooleanClause( 1248 getMultiTermQueryFilter(CmsSearchField.FIELD_CATEGORY, lowerCaseCategories), 1249 BooleanClause.Occur.MUST)); 1250 } 1251 1252 return filter; 1253 } 1254 1255 /** 1256 * Appends a date of creation filter to the given filter clause that matches the 1257 * given time range.<p> 1258 * 1259 * If the start time is equal to {@link Long#MIN_VALUE} and the end time is equal to {@link Long#MAX_VALUE} 1260 * than the original filter is left unchanged.<p> 1261 * 1262 * The original filter parameter is extended and also provided as return value.<p> 1263 * 1264 * @param filter the filter to extend 1265 * @param startTime start time of the range to search in 1266 * @param endTime end time of the range to search in 1267 * 1268 * @return the extended filter clause 1269 */ 1270 protected BooleanQuery.Builder appendDateCreatedFilter(BooleanQuery.Builder filter, long startTime, long endTime) { 1271 1272 // create special optimized sub-filter for the date last modified search 1273 Query dateFilter = createDateRangeFilter(CmsSearchField.FIELD_DATE_CREATED_LOOKUP, startTime, endTime); 1274 if (dateFilter != null) { 1275 // extend main filter with the created date filter 1276 filter.add(new BooleanClause(dateFilter, BooleanClause.Occur.MUST)); 1277 } 1278 1279 return filter; 1280 } 1281 1282 /** 1283 * Appends a date of last modification filter to the given filter clause that matches the 1284 * given time range.<p> 1285 * 1286 * If the start time is equal to {@link Long#MIN_VALUE} and the end time is equal to {@link Long#MAX_VALUE} 1287 * than the original filter is left unchanged.<p> 1288 * 1289 * The original filter parameter is extended and also provided as return value.<p> 1290 * 1291 * @param filter the filter to extend 1292 * @param startTime start time of the range to search in 1293 * @param endTime end time of the range to search in 1294 * 1295 * @return the extended filter clause 1296 */ 1297 protected BooleanQuery.Builder appendDateLastModifiedFilter( 1298 BooleanQuery.Builder filter, 1299 long startTime, 1300 long endTime) { 1301 1302 // create special optimized sub-filter for the date last modified search 1303 Query dateFilter = createDateRangeFilter(CmsSearchField.FIELD_DATE_LASTMODIFIED_LOOKUP, startTime, endTime); 1304 if (dateFilter != null) { 1305 // extend main filter with the created date filter 1306 filter.add(new BooleanClause(dateFilter, BooleanClause.Occur.MUST)); 1307 } 1308 1309 return filter; 1310 } 1311 1312 /** 1313 * Appends the a VFS path filter to the given filter clause that matches all given root paths.<p> 1314 * 1315 * In case the provided List is null or empty, the current request context site root is appended.<p> 1316 * 1317 * The original filter parameter is extended and also provided as return value.<p> 1318 * 1319 * @param cms the current OpenCms search context 1320 * @param filter the filter to extend 1321 * @param roots the VFS root paths that will compose the filter 1322 * 1323 * @return the extended filter clause 1324 */ 1325 protected BooleanQuery.Builder appendPathFilter(CmsObject cms, BooleanQuery.Builder filter, List<String> roots) { 1326 1327 // complete the search root 1328 List<Term> terms = new ArrayList<Term>(); 1329 if ((roots != null) && (roots.size() > 0)) { 1330 // add the all configured search roots with will request context 1331 for (int i = 0; i < roots.size(); i++) { 1332 String searchRoot = cms.getRequestContext().addSiteRoot(roots.get(i)); 1333 extendPathFilter(terms, searchRoot); 1334 } 1335 } else { 1336 // use the current site root as the search root 1337 extendPathFilter(terms, cms.getRequestContext().getSiteRoot()); 1338 // also add the shared folder (v 8.0) 1339 if (OpenCms.getSiteManager().getSharedFolder() != null) { 1340 extendPathFilter(terms, OpenCms.getSiteManager().getSharedFolder()); 1341 } 1342 } 1343 1344 // add the calculated path filter for the root path 1345 BooleanQuery.Builder build = new BooleanQuery.Builder(); 1346 terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD)); 1347 filter.add(new BooleanClause(build.build(), BooleanClause.Occur.MUST)); 1348 return filter; 1349 } 1350 1351 /** 1352 * Appends the a resource type filter to the given filter clause that matches all given resource types.<p> 1353 * 1354 * In case the provided List is null or empty, the original filter is left unchanged.<p> 1355 * 1356 * The original filter parameter is extended and also provided as return value.<p> 1357 * 1358 * @param cms the current OpenCms search context 1359 * @param filter the filter to extend 1360 * @param resourceTypes the resource types that will compose the filter 1361 * 1362 * @return the extended filter clause 1363 */ 1364 protected BooleanQuery.Builder appendResourceTypeFilter( 1365 CmsObject cms, 1366 BooleanQuery.Builder filter, 1367 List<String> resourceTypes) { 1368 1369 if ((resourceTypes != null) && (resourceTypes.size() > 0)) { 1370 // add query resource types (if required) 1371 filter.add( 1372 new BooleanClause( 1373 getMultiTermQueryFilter(CmsSearchField.FIELD_TYPE, resourceTypes), 1374 BooleanClause.Occur.MUST)); 1375 } 1376 1377 return filter; 1378 } 1379 1380 /** 1381 * Creates an optimized date range filter for the date of last modification or creation.<p> 1382 * 1383 * If the start date is equal to {@link Long#MIN_VALUE} and the end date is equal to {@link Long#MAX_VALUE} 1384 * than <code>null</code> is returned.<p> 1385 * 1386 * @param fieldName the name of the field to search 1387 * @param startTime start time of the range to search in 1388 * @param endTime end time of the range to search in 1389 * 1390 * @return an optimized date range filter for the date of last modification or creation 1391 */ 1392 protected Query createDateRangeFilter(String fieldName, long startTime, long endTime) { 1393 1394 Query filter = null; 1395 if ((startTime != Long.MIN_VALUE) || (endTime != Long.MAX_VALUE)) { 1396 // a date range has been set for this document search 1397 if (startTime == Long.MIN_VALUE) { 1398 // default start will always be "yyyy1231" in order to reduce term size 1399 Calendar cal = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone()); 1400 cal.setTimeInMillis(endTime); 1401 cal.set(cal.get(Calendar.YEAR) - MAX_YEAR_RANGE, 11, 31, 0, 0, 0); 1402 startTime = cal.getTimeInMillis(); 1403 } else if (endTime == Long.MAX_VALUE) { 1404 // default end will always be "yyyy0101" in order to reduce term size 1405 Calendar cal = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone()); 1406 cal.setTimeInMillis(startTime); 1407 cal.set(cal.get(Calendar.YEAR) + MAX_YEAR_RANGE, 0, 1, 0, 0, 0); 1408 endTime = cal.getTimeInMillis(); 1409 } 1410 1411 // get the list of all possible date range options 1412 List<String> dateRange = getDateRangeSpan(startTime, endTime); 1413 List<Term> terms = new ArrayList<Term>(); 1414 for (String range : dateRange) { 1415 terms.add(new Term(fieldName, range)); 1416 } 1417 // create the filter for the date 1418 BooleanQuery.Builder build = new BooleanQuery.Builder(); 1419 terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD)); 1420 filter = build.build(); 1421 } 1422 return filter; 1423 } 1424 1425 /** 1426 * Creates a backup of this index for optimized re-indexing of the whole content.<p> 1427 * 1428 * @return the path to the backup folder, or <code>null</code> in case no backup was created 1429 */ 1430 protected String createIndexBackup() { 1431 1432 if (!isBackupReindexing()) { 1433 // if no backup is generated we don't need to do anything 1434 return null; 1435 } 1436 1437 // check if the target directory already exists 1438 File file = new File(getPath()); 1439 if (!file.exists()) { 1440 // index does not exist yet, so we can't backup it 1441 return null; 1442 } 1443 String backupPath = getPath() + "_backup"; 1444 FSDirectory oldDir = null; 1445 FSDirectory newDir = null; 1446 try { 1447 // open file directory for Lucene 1448 oldDir = FSDirectory.open(file.toPath()); 1449 newDir = FSDirectory.open(Paths.get(backupPath)); 1450 for (String fileName : oldDir.listAll()) { 1451 newDir.copyFrom(oldDir, fileName, fileName, IOContext.DEFAULT); 1452 } 1453 } catch (Exception e) { 1454 LOG.error( 1455 Messages.get().getBundle().key(Messages.LOG_IO_INDEX_BACKUP_CREATE_3, getName(), getPath(), backupPath), 1456 e); 1457 backupPath = null; 1458 } finally { 1459 if (oldDir != null) { 1460 try { 1461 oldDir.close(); 1462 } catch (IOException e) { 1463 e.printStackTrace(); 1464 } 1465 } 1466 if (newDir != null) { 1467 try { 1468 newDir.close(); 1469 } catch (IOException e) { 1470 e.printStackTrace(); 1471 } 1472 } 1473 } 1474 return backupPath; 1475 } 1476 1477 /** 1478 * Creates a new index writer.<p> 1479 * 1480 * @param create if <code>true</code> a whole new index is created, if <code>false</code> an existing index is updated 1481 * @param report the report 1482 * 1483 * @return the created new index writer 1484 * 1485 * @throws CmsIndexException in case the writer could not be created 1486 * 1487 * @see #getIndexWriter(I_CmsReport, boolean) 1488 */ 1489 @Override 1490 protected I_CmsIndexWriter createIndexWriter(boolean create, I_CmsReport report) throws CmsIndexException { 1491 1492 IndexWriter indexWriter = null; 1493 FSDirectory dir = null; 1494 try { 1495 File f = new File(getPath()); 1496 if (!f.exists()) { 1497 f = f.getParentFile(); 1498 if ((f != null) && (!f.exists())) { 1499 f.mkdirs(); 1500 } 1501 1502 create = true; 1503 } 1504 1505 dir = FSDirectory.open(Paths.get(getPath())); 1506 IndexWriterConfig indexConfig = new IndexWriterConfig(getAnalyzer()); 1507 //indexConfig.setMergePolicy(mergePolicy); 1508 1509 if (m_luceneRAMBufferSizeMB != null) { 1510 indexConfig.setRAMBufferSizeMB(m_luceneRAMBufferSizeMB.doubleValue()); 1511 } 1512 if (create) { 1513 indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE); 1514 } else { 1515 indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); 1516 } 1517 // register the modified default similarity implementation 1518 indexConfig.setSimilarity(m_sim); 1519 1520 indexWriter = new IndexWriter(dir, indexConfig); 1521 } catch (Exception e) { 1522 if (dir != null) { 1523 try { 1524 dir.close(); 1525 } catch (IOException e1) { 1526 // TODO Auto-generated catch block 1527 e1.printStackTrace(); 1528 } 1529 } 1530 if (indexWriter != null) { 1531 try { 1532 indexWriter.close(); 1533 } catch (IOException closeExeception) { 1534 throw new CmsIndexException( 1535 Messages.get().container(Messages.ERR_IO_INDEX_WRITER_OPEN_2, getPath(), getName()), 1536 e); 1537 } 1538 } 1539 throw new CmsIndexException( 1540 Messages.get().container(Messages.ERR_IO_INDEX_WRITER_OPEN_2, getPath(), getName()), 1541 e); 1542 } 1543 1544 return new CmsLuceneIndexWriter(indexWriter, this); 1545 } 1546 1547 /** 1548 * Extends the given path query with another term for the given search root element.<p> 1549 * 1550 * @param terms the path filter to extend 1551 * @param searchRoot the search root to add to the path query 1552 */ 1553 protected void extendPathFilter(List<Term> terms, String searchRoot) { 1554 1555 if (!CmsResource.isFolder(searchRoot)) { 1556 searchRoot += "/"; 1557 } 1558 terms.add(new Term(CmsSearchField.FIELD_PARENT_FOLDERS, searchRoot)); 1559 } 1560 1561 /** 1562 * Generates the directory on the RFS for this index.<p> 1563 * 1564 * @return the directory on the RFS for this index 1565 */ 1566 protected String generateIndexDirectory() { 1567 1568 return OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf( 1569 OpenCms.getSearchManager().getDirectory() + "/" + getName()); 1570 } 1571 1572 /** 1573 * Returns a cached Lucene term query filter for the given field and terms.<p> 1574 * 1575 * @param field the field to use 1576 * @param terms the term to use 1577 * 1578 * @return a cached Lucene term query filter for the given field and terms 1579 */ 1580 protected Query getMultiTermQueryFilter(String field, List<String> terms) { 1581 1582 return getMultiTermQueryFilter(field, null, terms); 1583 } 1584 1585 /** 1586 * Returns a cached Lucene term query filter for the given field and terms.<p> 1587 * 1588 * @param field the field to use 1589 * @param terms the term to use 1590 * 1591 * @return a cached Lucene term query filter for the given field and terms 1592 */ 1593 protected Query getMultiTermQueryFilter(String field, String terms) { 1594 1595 return getMultiTermQueryFilter(field, terms, null); 1596 } 1597 1598 /** 1599 * Returns a cached Lucene term query filter for the given field and terms.<p> 1600 * 1601 * @param field the field to use 1602 * @param termsStr the terms to use as a String separated by a space ' ' char 1603 * @param termsList the list of terms to use 1604 * 1605 * @return a cached Lucene term query filter for the given field and terms 1606 */ 1607 protected Query getMultiTermQueryFilter(String field, String termsStr, List<String> termsList) { 1608 1609 if (termsStr == null) { 1610 StringBuffer buf = new StringBuffer(64); 1611 for (int i = 0; i < termsList.size(); i++) { 1612 if (i > 0) { 1613 buf.append(' '); 1614 } 1615 buf.append(termsList.get(i)); 1616 } 1617 termsStr = buf.toString(); 1618 } 1619 Query result = m_displayFilters.get( 1620 (new StringBuffer(64)).append(field).append('|').append(termsStr).toString()); 1621 if (result == null) { 1622 List<Term> terms = new ArrayList<Term>(); 1623 if (termsList == null) { 1624 termsList = CmsStringUtil.splitAsList(termsStr, ' '); 1625 } 1626 for (int i = 0; i < termsList.size(); i++) { 1627 terms.add(new Term(field, termsList.get(i))); 1628 } 1629 1630 BooleanQuery.Builder build = new BooleanQuery.Builder(); 1631 terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD)); 1632 Query termsQuery = build.build(); //termsFilter 1633 1634 try { 1635 result = termsQuery.createWeight(m_indexSearcher, ScoreMode.COMPLETE_NO_SCORES, 1).getQuery(); 1636 m_displayFilters.put(field + termsStr, result); 1637 } catch (IOException e) { 1638 // TODO don't know what happend 1639 e.printStackTrace(); 1640 } 1641 } 1642 return result; 1643 } 1644 1645 /** 1646 * Checks if the OpenCms resource referenced by the result document can be read 1647 * by the user of the given OpenCms context. 1648 * 1649 * Returns the referenced <code>CmsResource</code> or <code>null</code> if 1650 * the user is not permitted to read the resource.<p> 1651 * 1652 * @param cms the OpenCms user context to use for permission testing 1653 * @param doc the search result document to check 1654 * 1655 * @return the referenced <code>CmsResource</code> or <code>null</code> if the user is not permitted 1656 */ 1657 protected CmsResource getResource(CmsObject cms, I_CmsSearchDocument doc) { 1658 1659 // check if the resource exits in the VFS, 1660 // this will implicitly check read permission and if the resource was deleted 1661 CmsResourceFilter filter = CmsResourceFilter.DEFAULT; 1662 if (isRequireViewPermission()) { 1663 filter = CmsResourceFilter.DEFAULT_ONLY_VISIBLE; 1664 } else if (isIgnoreExpiration()) { 1665 filter = CmsResourceFilter.IGNORE_EXPIRATION; 1666 } 1667 1668 return getResource(cms, doc, filter); 1669 } 1670 1671 /** 1672 * Checks if the OpenCms resource referenced by the result document can be read 1673 * by the user of the given OpenCms context. 1674 * 1675 * Returns the referenced <code>CmsResource</code> or <code>null</code> if 1676 * the user is not permitted to read the resource.<p> 1677 * 1678 * @param cms the OpenCms user context to use for permission testing 1679 * @param doc the search result document to check 1680 * @param filter the resource filter to apply 1681 * 1682 * @return the referenced <code>CmsResource</code> or <code>null</code> if the user is not permitted 1683 */ 1684 protected CmsResource getResource(CmsObject cms, I_CmsSearchDocument doc, CmsResourceFilter filter) { 1685 1686 try { 1687 CmsObject clone = OpenCms.initCmsObject(cms); 1688 clone.getRequestContext().setSiteRoot(""); 1689 return clone.readResource(doc.getPath(), filter); 1690 } catch (CmsException e) { 1691 // Do nothing 1692 } 1693 1694 return null; 1695 } 1696 1697 /** 1698 * Returns a cached Lucene term query filter for the given field and term.<p> 1699 * 1700 * @param field the field to use 1701 * @param term the term to use 1702 * 1703 * @return a cached Lucene term query filter for the given field and term 1704 */ 1705 protected Query getTermQueryFilter(String field, String term) { 1706 1707 return getMultiTermQueryFilter(field, term, Collections.singletonList(term)); 1708 } 1709 1710 /** 1711 * Checks if the OpenCms resource referenced by the result document can be read 1712 * be the user of the given OpenCms context.<p> 1713 * 1714 * @param cms the OpenCms user context to use for permission testing 1715 * @param doc the search result document to check 1716 * @return <code>true</code> if the user has read permissions to the resource 1717 */ 1718 protected boolean hasReadPermission(CmsObject cms, I_CmsSearchDocument doc) { 1719 1720 // If no permission check is needed: the document can be read 1721 // Else try to read the resource if this is not possible the user does not have enough permissions 1722 return !needsPermissionCheck(doc) ? true : (null != getResource(cms, doc)); 1723 } 1724 1725 /** 1726 * Closes the index searcher for this index.<p> 1727 * 1728 * @see #indexSearcherOpen(String) 1729 */ 1730 protected synchronized void indexSearcherClose() { 1731 1732 indexSearcherClose(m_indexSearcher); 1733 } 1734 1735 /** 1736 * Closes the given Lucene index searcher.<p> 1737 * 1738 * @param searcher the searcher to close 1739 */ 1740 protected synchronized void indexSearcherClose(IndexSearcher searcher) { 1741 1742 // in case there is an index searcher available close it 1743 if ((searcher != null) && (searcher.getIndexReader() != null)) { 1744 try { 1745 searcher.getIndexReader().close(); 1746 } catch (Exception e) { 1747 LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_CLOSE_1, getName()), e); 1748 } 1749 } 1750 } 1751 1752 /** 1753 * Initializes the index searcher for this index.<p> 1754 * 1755 * In case there is an index searcher still open, it is closed first.<p> 1756 * 1757 * For performance reasons, one instance of the index searcher should be kept 1758 * for all searches. However, if the index is updated or changed 1759 * this searcher instance needs to be re-initialized.<p> 1760 * 1761 * @param path the path to the index directory 1762 */ 1763 protected synchronized void indexSearcherOpen(String path) { 1764 1765 IndexSearcher oldSearcher = null; 1766 Directory indexDirectory = null; 1767 try { 1768 indexDirectory = FSDirectory.open(Paths.get(path)); 1769 if (DirectoryReader.indexExists(indexDirectory)) { 1770 IndexReader reader = UninvertingReader.wrap( 1771 DirectoryReader.open(indexDirectory), 1772 createUninvertingMap()); 1773 if (m_indexSearcher != null) { 1774 // store old searcher instance to close it later 1775 oldSearcher = m_indexSearcher; 1776 } 1777 m_indexSearcher = new IndexSearcher(reader); 1778 m_indexSearcher.setSimilarity(m_sim); 1779 m_displayFilters = new HashMap<>(); 1780 } 1781 } catch (IOException e) { 1782 LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_1, getName()), e); 1783 if (indexDirectory != null) { 1784 try { 1785 indexDirectory.close(); 1786 } catch (IOException closeException) { 1787 // do nothing 1788 } 1789 } 1790 } 1791 if (oldSearcher != null) { 1792 // close the old searcher if required 1793 indexSearcherClose(oldSearcher); 1794 } 1795 } 1796 1797 /** 1798 * Reopens the index search reader for this index, required after the index has been changed.<p> 1799 * 1800 * @see #indexSearcherOpen(String) 1801 */ 1802 protected synchronized void indexSearcherUpdate() { 1803 1804 IndexSearcher oldSearcher = m_indexSearcher; 1805 if ((oldSearcher != null) && (oldSearcher.getIndexReader() != null)) { 1806 // in case there is an index searcher available close it 1807 try { 1808 if (oldSearcher.getIndexReader() instanceof DirectoryReader) { 1809 IndexReader newReader = DirectoryReader.openIfChanged( 1810 (DirectoryReader)oldSearcher.getIndexReader()); 1811 if (newReader != null) { 1812 m_indexSearcher = new IndexSearcher(newReader); 1813 m_indexSearcher.setSimilarity(m_sim); 1814 indexSearcherClose(oldSearcher); 1815 } 1816 } 1817 } catch (Exception e) { 1818 LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_REOPEN_1, getName()), e); 1819 } 1820 } else { 1821 // make sure we end up with an open index searcher / reader 1822 indexSearcherOpen(getPath()); 1823 } 1824 } 1825 1826 /** 1827 * Checks if the document is in the time range specified in the search parameters.<p> 1828 * 1829 * The creation date and/or the last modification date are checked.<p> 1830 * 1831 * @param doc the document to check the dates against the given time range 1832 * @param params the search parameters where the time ranges are specified 1833 * 1834 * @return true if document is in time range or not time range set otherwise false 1835 */ 1836 protected boolean isInTimeRange(Document doc, CmsSearchParameters params) { 1837 1838 if (!isCheckingTimeRange()) { 1839 // time range check disabled 1840 return true; 1841 } 1842 1843 try { 1844 // check the creation date of the document against the given time range 1845 Date dateCreated = DateTools.stringToDate(doc.getField(CmsSearchField.FIELD_DATE_CREATED).stringValue()); 1846 if (dateCreated.getTime() < params.getMinDateCreated()) { 1847 return false; 1848 } 1849 if (dateCreated.getTime() > params.getMaxDateCreated()) { 1850 return false; 1851 } 1852 1853 // check the last modification date of the document against the given time range 1854 Date dateLastModified = DateTools.stringToDate( 1855 doc.getField(CmsSearchField.FIELD_DATE_LASTMODIFIED).stringValue()); 1856 if (dateLastModified.getTime() < params.getMinDateLastModified()) { 1857 return false; 1858 } 1859 if (dateLastModified.getTime() > params.getMaxDateLastModified()) { 1860 return false; 1861 } 1862 1863 } catch (ParseException ex) { 1864 // date could not be parsed -> doc is in time range 1865 } 1866 1867 return true; 1868 } 1869 1870 /** 1871 * Checks if the score for the results must be calculated based on the provided sort option.<p> 1872 * 1873 * Since Lucene 3 apparently the score is no longer calculated by default, but only if the 1874 * searcher is explicitly told so. This methods checks if, based on the given sort, 1875 * the score must be calculated.<p> 1876 * 1877 * @param searcher the index searcher to prepare 1878 * @param sort the sort option to use 1879 * 1880 * @return true if the sort option should be used 1881 */ 1882 protected boolean isSortScoring(IndexSearcher searcher, Sort sort) { 1883 1884 boolean doScoring = false; 1885 if (sort != null) { 1886 if ((sort == CmsSearchParameters.SORT_DEFAULT) || (sort == CmsSearchParameters.SORT_TITLE)) { 1887 // these default sorts do need score calculation 1888 doScoring = true; 1889 } else if ((sort == CmsSearchParameters.SORT_DATE_CREATED) 1890 || (sort == CmsSearchParameters.SORT_DATE_LASTMODIFIED)) { 1891 // these default sorts don't need score calculation 1892 doScoring = false; 1893 } else { 1894 // for all non-defaults: check if the score field is present, in that case we must calculate the score 1895 SortField[] fields = sort.getSort(); 1896 for (SortField field : fields) { 1897 if (field == SortField.FIELD_SCORE) { 1898 doScoring = true; 1899 break; 1900 } 1901 } 1902 } 1903 } 1904 return doScoring; 1905 } 1906 1907 /** 1908 * Checks if the OpenCms resource referenced by the result document needs to be checked.<p> 1909 * 1910 * @param doc the search result document to check 1911 * 1912 * @return <code>true</code> if the document needs to be checked <code>false</code> otherwise 1913 */ 1914 protected boolean needsPermissionCheck(I_CmsSearchDocument doc) { 1915 1916 if (!isCheckingPermissions()) { 1917 // no permission check is performed at all 1918 return false; 1919 } 1920 1921 if ((doc.getType() == null) || (doc.getPath() == null)) { 1922 // permission check needs only to be performed for VFS documents that contain both fields 1923 return false; 1924 } 1925 1926 if (!I_CmsSearchDocument.VFS_DOCUMENT_KEY_PREFIX.equals(doc.getType()) 1927 && !OpenCms.getResourceManager().hasResourceType(doc.getType())) { 1928 // this is an unknown VFS resource type (also not the generic "VFS" type of OpenCms before 7.0) 1929 return false; 1930 } 1931 return true; 1932 } 1933 1934 /** 1935 * Removes the given backup folder of this index.<p> 1936 * 1937 * @param path the backup folder to remove 1938 */ 1939 protected void removeIndexBackup(String path) { 1940 1941 if (!isBackupReindexing()) { 1942 // if no backup is generated we don't need to do anything 1943 return; 1944 } 1945 1946 // check if the target directory already exists 1947 File file = new File(path); 1948 if (!file.exists()) { 1949 // index does not exist yet 1950 return; 1951 } 1952 try { 1953 FSDirectory dir = FSDirectory.open(file.toPath()); 1954 dir.close(); 1955 CmsFileUtil.purgeDirectory(file); 1956 } catch (Exception e) { 1957 LOG.error(Messages.get().getBundle().key(Messages.LOG_IO_INDEX_BACKUP_REMOVE_2, getName(), path), e); 1958 } 1959 } 1960 1961 /** 1962 * Generates the uninverting map and adds it to the field configuration. 1963 * @return the generated uninverting map 1964 * 1965 * @see CmsSearchField#addUninvertingMappings(Map) 1966 */ 1967 private Map<String, Type> createUninvertingMap() { 1968 1969 Map<String, UninvertingReader.Type> uninvertingMap = new HashMap<String, UninvertingReader.Type>(); 1970 CmsSearchField.addUninvertingMappings(uninvertingMap); 1971 getFieldConfiguration().addUninvertingMappings(uninvertingMap); 1972 return uninvertingMap; 1973 } 1974 1975}