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            // we truncate to seconds, since the index stores no milliseconds
555            // and it seems practically irrelevant that a content is updated twice in a second.
556            if ((contentDateIndex / 1000L) == (resource.getDateContent() / 1000L)) {
557                // extract stored content blob from index
558                return CmsExtractionResult.fromBytes(oldDoc.getContentBlob());
559            }
560        }
561        return null;
562    }
563
564    /**
565     * Returns a document by document ID.<p>
566     *
567     * @param docId the id to get the document for
568     *
569     * @return the CMS specific document
570     */
571    public I_CmsSearchDocument getDocument(int docId) {
572
573        try {
574            IndexSearcher searcher = getSearcher();
575            return new CmsLuceneDocument(searcher.doc(docId));
576        } catch (IOException e) {
577            // ignore, return null and assume document was not found
578        }
579        return null;
580    }
581
582    /**
583     * Returns the Lucene document with the given root path from the index.<p>
584     *
585     * @param rootPath the root path of the document to get
586     *
587     * @return the Lucene document with the given root path from the index
588     *
589     * @deprecated Use {@link #getDocument(String, String)} instead and provide {@link org.opencms.search.fields.CmsLuceneField#FIELD_PATH} as field to search in
590     */
591    @Deprecated
592    public Document getDocument(String rootPath) {
593
594        if (getDocument(CmsSearchField.FIELD_PATH, rootPath) != null) {
595            return (Document)getDocument(CmsSearchField.FIELD_PATH, rootPath).getDocument();
596        }
597        return null;
598    }
599
600    /**
601     * Returns the first document where the given term matches the selected index field.<p>
602     *
603     * Use this method to search for documents which have unique field values, like a unique id.<p>
604     *
605     * @param field the field to search in
606     * @param term the term to search for
607     *
608     * @return the first document where the given term matches the selected index field
609     */
610    public I_CmsSearchDocument getDocument(String field, String term) {
611
612        Document result = null;
613        IndexSearcher searcher = getSearcher();
614        if (searcher != null) {
615            // search for an exact match on the selected field
616            Term resultTerm = new Term(field, term);
617            try {
618                TopDocs hits = searcher.search(new TermQuery(resultTerm), 1);
619                if (hits.scoreDocs.length > 0) {
620                    result = searcher.doc(hits.scoreDocs[0].doc);
621                }
622            } catch (IOException e) {
623                // ignore, return null and assume document was not found
624            }
625        }
626        if (result != null) {
627            return new CmsLuceneDocument(result);
628        }
629        return null;
630    }
631
632    /**
633     * Returns the language locale for the given resource in this index.<p>
634     *
635     * @param cms the current OpenCms user context
636     * @param resource the resource to check
637     * @param availableLocales a list of locales supported by the resource
638     *
639     * @return the language locale for the given resource in this index
640     */
641    @Override
642    public Locale getLocaleForResource(CmsObject cms, CmsResource resource, List<Locale> availableLocales) {
643
644        Locale result;
645        List<Locale> defaultLocales = OpenCms.getLocaleManager().getDefaultLocales(cms, resource);
646        List<Locale> locales = availableLocales;
647        if ((locales == null) || (locales.size() == 0)) {
648            locales = defaultLocales;
649        }
650        result = OpenCms.getLocaleManager().getBestMatchingLocale(getLocale(), defaultLocales, locales);
651        return result;
652    }
653
654    /**
655    * Returns the language locale of the index as a String.<p>
656    *
657    * @return the language locale of the index as a String
658    *
659    * @see #getLocale()
660    */
661    public String getLocaleString() {
662
663        return getLocale().toString();
664    }
665
666    /**
667     * Indicates the number of how many hits are loaded at maximum.<p>
668     *
669     * The number of maximum documents to load from the index
670     * must be specified. The default of this setting is {@link CmsSearchIndex#MAX_HITS_DEFAULT} (5000).
671     * This means that at maximum 5000 results are returned from the index.
672     * Please note that this number may be reduced further because of OpenCms read permissions
673     * or per-user file visibility settings not controlled in the index.<p>
674     *
675     * @return the number of how many hits are loaded at maximum
676     *
677     * @since 7.5.1
678     */
679    public int getMaxHits() {
680
681        return m_maxHits;
682    }
683
684    /**
685     * Returns the path where this index stores it's data in the "real" file system.<p>
686     *
687     * @return the path where this index stores it's data in the "real" file system
688     */
689    @Override
690    public String getPath() {
691
692        if (super.getPath() == null) {
693            setPath(generateIndexDirectory());
694        }
695        return super.getPath();
696    }
697
698    /**
699     * Returns the Thread priority for this search index.<p>
700     *
701     * @return the Thread priority for this search index
702     */
703    public int getPriority() {
704
705        return m_priority;
706    }
707
708    /**
709     * Returns the Lucene index searcher used for this search index.<p>
710     *
711     * @return the Lucene index searcher used for this search index
712     */
713    public IndexSearcher getSearcher() {
714
715        return m_indexSearcher;
716    }
717
718    /**
719     * @see org.opencms.search.A_CmsSearchIndex#initialize()
720     */
721    @Override
722    public void initialize() throws CmsSearchException {
723
724        super.initialize();
725
726        // get the configured analyzer and apply the the field configuration analyzer wrapper
727        @SuppressWarnings("resource")
728        Analyzer baseAnalyzer = OpenCms.getSearchManager().getAnalyzer(getLocale());
729
730        if (getFieldConfiguration() instanceof CmsLuceneFieldConfiguration) {
731            CmsLuceneFieldConfiguration fc = (CmsLuceneFieldConfiguration)getFieldConfiguration();
732            setAnalyzer(fc.getAnalyzer(baseAnalyzer));
733        }
734    }
735
736    /**
737     * Returns <code>true</code> if backup re-indexing is done by this index.<p>
738     *
739     * This is an optimization method by which the old extracted content is
740     * reused in order to save performance when re-indexing.<p>
741     *
742     * @return  <code>true</code> if backup re-indexing is done by this index
743     *
744     * @since 7.5.1
745     */
746    public boolean isBackupReindexing() {
747
748        return m_backupReindexing;
749    }
750
751    /**
752     * Returns <code>true</code> if permissions are checked for search results by this index.<p>
753     *
754     * If permission checks are not required, they can be turned off in the index search configuration parameters
755     * in <code>opencms-search.xml</code>. Not checking permissions will improve performance.<p>
756     *
757     * This is can be of use in scenarios when you know that all search results are always readable,
758     * which is usually true for public websites that do not have personalized accounts.<p>
759     *
760     * Please note that even if a result is returned where the current user has no read permissions,
761     * the user can not actually access this document. It will only appear in the search result list,
762     * but if the user clicks the link to open the document he will get an error.<p>
763     *
764     *
765     * @return <code>true</code> if permissions are checked for search results by this index
766     */
767    public boolean isCheckingPermissions() {
768
769        return m_checkPermissions;
770    }
771
772    /**
773     * Returns <code>true</code> if the document time range is checked with a granularity level of seconds
774     * for search results by this index.<p>
775     *
776     * Since OpenCms 8.0, time range checks are always done if {@link CmsSearchParameters#setMinDateLastModified(long)}
777     * or any of the corresponding methods are used.
778     * This is done very efficiently using optimized Lucene filers.
779     * However, the granularity of these checks are done only on a daily
780     * basis, which means that you can only find "changes made yesterday" but not "changes made last hour".
781     * For normal limitation of search results, a daily granularity should be enough.<p>
782     *
783     * If time range checks with a granularity level of seconds are required,
784     * they can be turned on in the index search configuration parameters
785     * in <code>opencms-search.xml</code>.
786     * Not checking the time range  with a granularity level of seconds will improve performance.<p>
787     *
788     * By default the granularity level of seconds is turned off since OpenCms 8.0<p>
789     *
790     * @return <code>true</code> if the document time range is checked  with a granularity level of seconds for search results by this index
791     */
792    public boolean isCheckingTimeRange() {
793
794        return m_checkTimeRange;
795    }
796
797    /**
798     * Returns the checkPermissions.<p>
799     *
800     * @return the checkPermissions
801     */
802    public boolean isCheckPermissions() {
803
804        return m_checkPermissions;
805    }
806
807    /**
808     * Returns <code>true</code> if an excerpt is generated by this index.<p>
809     *
810     * If no except is required, generation can be turned off in the index search configuration parameters
811     * in <code>opencms-search.xml</code>. Not generating an excerpt will improve performance.<p>
812     *
813     * @return <code>true</code> if an excerpt is generated by this index
814     */
815    public boolean isCreatingExcerpt() {
816
817        return m_createExcerpt;
818    }
819
820    /**
821     * Returns the ignoreExpiration.<p>
822     *
823     * @return the ignoreExpiration
824     */
825    public boolean isIgnoreExpiration() {
826
827        return m_ignoreExpiration;
828    }
829
830    /**
831     * @see org.opencms.search.A_CmsSearchIndex#isInitialized()
832     */
833    @Override
834    public boolean isInitialized() {
835
836        return super.isInitialized() && (null != getPath());
837    }
838
839    /**
840     * Returns <code>true</code> if a resource requires read permission to be included in the result list.<p>
841     *
842     * @return <code>true</code> if a resource requires read permission to be included in the result list
843     */
844    public boolean isRequireViewPermission() {
845
846        return m_requireViewPermission;
847    }
848
849    /**
850     * @see org.opencms.search.A_CmsSearchIndex#onIndexChanged(boolean)
851     */
852    @Override
853    public void onIndexChanged(boolean force) {
854
855        if (force) {
856            indexSearcherOpen(getPath());
857        } else {
858            indexSearcherUpdate();
859        }
860    }
861
862    /**
863     * Performs a search on the index within the given fields.<p>
864     *
865     * The result is returned as List with entries of type I_CmsSearchResult.<p>
866     *
867     * @param cms the current user's Cms object
868     * @param params the parameters to use for the search
869     *
870     * @return the List of results found or an empty list
871     *
872     * @throws CmsSearchException if something goes wrong
873     */
874    public CmsSearchResultList search(CmsObject cms, CmsSearchParameters params) throws CmsSearchException {
875
876        long timeTotal = -System.currentTimeMillis();
877        long timeLucene;
878        long timeResultProcessing;
879
880        if (LOG.isDebugEnabled()) {
881            LOG.debug(Messages.get().getBundle().key(Messages.LOG_SEARCH_PARAMS_2, params, getName()));
882        }
883
884        // the hits found during the search
885        TopDocs hits;
886
887        // storage for the results found
888        CmsSearchResultList searchResults = new CmsSearchResultList();
889
890        int previousPriority = Thread.currentThread().getPriority();
891
892        try {
893            // copy the user OpenCms context
894            CmsObject searchCms = OpenCms.initCmsObject(cms);
895
896            if (getPriority() > 0) {
897                // change thread priority in order to reduce search impact on overall system performance
898                Thread.currentThread().setPriority(getPriority());
899            }
900
901            // change the project
902            searchCms.getRequestContext().setCurrentProject(searchCms.readProject(getProject()));
903
904            timeLucene = -System.currentTimeMillis();
905
906            // several search options are searched using filters
907            BooleanQuery.Builder builder = new BooleanQuery.Builder();
908            // append root path filter
909            builder = appendPathFilter(searchCms, builder, params.getRoots());
910            // append category filter
911            builder = appendCategoryFilter(searchCms, builder, params.getCategories());
912            // append resource type filter
913            builder = appendResourceTypeFilter(searchCms, builder, params.getResourceTypes());
914
915            // append date last modified filter
916            builder = appendDateLastModifiedFilter(
917                builder,
918                params.getMinDateLastModified(),
919                params.getMaxDateLastModified());
920            // append date created filter
921            builder = appendDateCreatedFilter(builder, params.getMinDateCreated(), params.getMaxDateCreated());
922
923            // the search query to use, will be constructed in the next lines
924            Query query = null;
925            // store separate fields query for excerpt highlighting
926            Query fieldsQuery = null;
927
928            // get an index searcher that is certainly up to date
929            indexSearcherUpdate();
930            IndexSearcher searcher = getSearcher();
931
932            if (!params.isIgnoreQuery()) {
933                // since OpenCms 8 the query can be empty in which case only filters are used for the result
934                if (params.getParsedQuery() != null) {
935                    // the query was already build, re-use it
936                    QueryParser p = new QueryParser(CmsSearchField.FIELD_CONTENT, getAnalyzer());
937                    fieldsQuery = p.parse(params.getParsedQuery());
938                } else if (params.getFieldQueries() != null) {
939                    // each field has an individual query
940                    BooleanQuery.Builder mustOccur = null;
941                    BooleanQuery.Builder shouldOccur = null;
942                    for (CmsSearchParameters.CmsSearchFieldQuery fq : params.getFieldQueries()) {
943                        // add one sub-query for each defined field
944                        QueryParser p = new QueryParser(fq.getFieldName(), getAnalyzer());
945                        // first generate the combined keyword query
946                        Query keywordQuery = null;
947                        if (fq.getSearchTerms().size() == 1) {
948                            // this is just a single size keyword list
949                            keywordQuery = p.parse(fq.getSearchTerms().get(0));
950                        } else {
951                            // multiple size keyword list
952                            BooleanQuery.Builder keywordListQuery = new BooleanQuery.Builder();
953                            for (String keyword : fq.getSearchTerms()) {
954                                keywordListQuery.add(p.parse(keyword), fq.getTermOccur());
955                            }
956                            keywordQuery = keywordListQuery.build();
957                        }
958                        if (BooleanClause.Occur.SHOULD.equals(fq.getOccur())) {
959                            if (shouldOccur == null) {
960                                shouldOccur = new BooleanQuery.Builder();
961                            }
962                            shouldOccur.add(keywordQuery, fq.getOccur());
963                        } else {
964                            if (mustOccur == null) {
965                                mustOccur = new BooleanQuery.Builder();
966                            }
967                            mustOccur.add(keywordQuery, fq.getOccur());
968                        }
969                    }
970                    BooleanQuery.Builder booleanFieldsQuery = new BooleanQuery.Builder();
971                    if (mustOccur != null) {
972                        booleanFieldsQuery.add(mustOccur.build(), BooleanClause.Occur.MUST);
973                    }
974                    if (shouldOccur != null) {
975                        booleanFieldsQuery.add(shouldOccur.build(), BooleanClause.Occur.MUST);
976                    }
977                    fieldsQuery = searcher.rewrite(booleanFieldsQuery.build());
978                } else if ((params.getFields() != null) && (params.getFields().size() > 0)) {
979                    // no individual field queries have been defined, so use one query for all fields
980                    BooleanQuery.Builder booleanFieldsQuery = new BooleanQuery.Builder();
981                    // this is a "regular" query over one or more fields
982                    // add one sub-query for each of the selected fields, e.g. "content", "title" etc.
983                    for (int i = 0; i < params.getFields().size(); i++) {
984                        QueryParser p = new QueryParser(params.getFields().get(i), getAnalyzer());
985                        p.setMultiTermRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_REWRITE);
986                        booleanFieldsQuery.add(p.parse(params.getQuery()), BooleanClause.Occur.SHOULD);
987                    }
988                    fieldsQuery = searcher.rewrite(booleanFieldsQuery.build());
989                } else {
990                    // if no fields are provided, just use the "content" field by default
991                    QueryParser p = new QueryParser(CmsSearchField.FIELD_CONTENT, getAnalyzer());
992                    fieldsQuery = searcher.rewrite(p.parse(params.getQuery()));
993                }
994
995                // finally set the main query to the fields query
996                // please note that we still need both variables in case the query is a MatchAllDocsQuery - see below
997                query = fieldsQuery;
998            }
999
1000            if (LOG.isDebugEnabled()) {
1001                LOG.debug(Messages.get().getBundle().key(Messages.LOG_BASE_QUERY_1, query));
1002            }
1003
1004            if (query == null) {
1005                // if no text query is set, then we match all documents
1006                query = new MatchAllDocsQuery();
1007            } else {
1008                // store the parsed query for page browsing
1009                params.setParsedQuery(query.toString(CmsSearchField.FIELD_CONTENT));
1010            }
1011
1012            // build the final query
1013            final BooleanQuery.Builder finalQueryBuilder = new BooleanQuery.Builder();
1014            finalQueryBuilder.add(query, BooleanClause.Occur.MUST);
1015            finalQueryBuilder.add(builder.build(), BooleanClause.Occur.FILTER);
1016            final BooleanQuery finalQuery = finalQueryBuilder.build();
1017
1018            // collect the categories
1019            CmsSearchCategoryCollector categoryCollector;
1020            if (params.isCalculateCategories()) {
1021                // USE THIS OPTION WITH CAUTION
1022                // this may slow down searched by an order of magnitude
1023                categoryCollector = new CmsSearchCategoryCollector(searcher);
1024                // perform a first search to collect the categories
1025                searcher.search(finalQuery, categoryCollector);
1026                // store the result
1027                searchResults.setCategories(categoryCollector.getCategoryCountResult());
1028            }
1029
1030            // get maxScore first, since Lucene 8, it's not computed automatically anymore
1031            TopDocs scoreHits = searcher.search(query, 1);
1032            float maxScore = scoreHits.scoreDocs.length == 0 ? Float.NaN : scoreHits.scoreDocs[0].score;
1033            // perform the search operation
1034            if ((params.getSort() == null) || (params.getSort() == CmsSearchParameters.SORT_DEFAULT)) {
1035                // apparently scoring is always enabled by Lucene if no sort order is provided
1036                hits = searcher.search(finalQuery, getMaxHits());
1037            } else {
1038                // if  a sort order is provided, we must check if scoring must be calculated by the searcher
1039                boolean isSortScore = isSortScoring(searcher, params.getSort());
1040                hits = searcher.search(finalQuery, getMaxHits(), params.getSort(), isSortScore);
1041            }
1042
1043            timeLucene += System.currentTimeMillis();
1044            timeResultProcessing = -System.currentTimeMillis();
1045
1046            if (hits != null) {
1047                long hitCount = hits.totalHits.value > hits.scoreDocs.length
1048                ? hits.scoreDocs.length
1049                : hits.totalHits.value;
1050                int page = params.getSearchPage();
1051                long start = -1, end = -1;
1052                if ((params.getMatchesPerPage() > 0) && (page > 0) && (hitCount > 0)) {
1053                    // calculate the final size of the search result
1054                    start = params.getMatchesPerPage() * (page - 1);
1055                    end = start + params.getMatchesPerPage();
1056                    // ensure that both i and n are inside the range of foundDocuments.size()
1057                    start = (start > hitCount) ? hitCount : start;
1058                    end = (end > hitCount) ? hitCount : end;
1059                } else {
1060                    // return all found documents in the search result
1061                    start = 0;
1062                    end = hitCount;
1063                }
1064
1065                Set<String> returnFields = ((CmsLuceneFieldConfiguration)getFieldConfiguration()).getReturnFields();
1066                Set<String> excerptFields = ((CmsLuceneFieldConfiguration)getFieldConfiguration()).getExcerptFields();
1067
1068                long visibleHitCount = hitCount;
1069                for (int i = 0, cnt = 0; (i < hitCount) && (cnt < end); i++) {
1070                    try {
1071                        Document doc = searcher.doc(hits.scoreDocs[i].doc, returnFields);
1072                        I_CmsSearchDocument searchDoc = new CmsLuceneDocument(doc);
1073                        searchDoc.setScore(hits.scoreDocs[i].score);
1074                        if ((isInTimeRange(doc, params)) && (hasReadPermission(searchCms, searchDoc))) {
1075                            // user has read permission
1076                            if (cnt >= start) {
1077                                // do not use the resource to obtain the raw content, read it from the lucene document!
1078                                String excerpt = null;
1079                                if (isCreatingExcerpt() && (fieldsQuery != null)) {
1080                                    Document exDoc = searcher.doc(hits.scoreDocs[i].doc, excerptFields);
1081                                    I_CmsTermHighlighter highlighter = OpenCms.getSearchManager().getHighlighter();
1082                                    excerpt = highlighter.getExcerpt(exDoc, this, params, fieldsQuery, getAnalyzer());
1083                                }
1084                                int score = Math.round(
1085                                    (maxScore != Float.NaN ? (hits.scoreDocs[i].score / maxScore) * 100f : 0));
1086                                searchResults.add(new CmsSearchResult(score, doc, excerpt));
1087                            }
1088                            cnt++;
1089                        } else {
1090                            visibleHitCount--;
1091                        }
1092                    } catch (Exception e) {
1093                        // should not happen, but if it does we want to go on with the next result nevertheless
1094                        if (LOG.isWarnEnabled()) {
1095                            LOG.warn(Messages.get().getBundle().key(Messages.LOG_RESULT_ITERATION_FAILED_0), e);
1096                        }
1097                    }
1098                }
1099
1100                // save the total count of search results
1101                searchResults.setHitCount((int)visibleHitCount);
1102            } else {
1103                searchResults.setHitCount(0);
1104            }
1105
1106            timeResultProcessing += System.currentTimeMillis();
1107        } catch (RuntimeException e) {
1108            throw new CmsSearchException(Messages.get().container(Messages.ERR_SEARCH_PARAMS_1, params), e);
1109        } catch (Exception e) {
1110            throw new CmsSearchException(Messages.get().container(Messages.ERR_SEARCH_PARAMS_1, params), e);
1111        } finally {
1112
1113            // re-set thread to previous priority
1114            Thread.currentThread().setPriority(previousPriority);
1115        }
1116
1117        if (LOG.isDebugEnabled()) {
1118            timeTotal += System.currentTimeMillis();
1119            Object[] logParams = new Object[] {
1120                Long.valueOf(hits == null ? 0 : hits.totalHits.value),
1121                Long.valueOf(timeTotal),
1122                Long.valueOf(timeLucene),
1123                Long.valueOf(timeResultProcessing)};
1124            LOG.debug(Messages.get().getBundle().key(Messages.LOG_STAT_RESULTS_TIME_4, logParams));
1125        }
1126
1127        return searchResults;
1128    }
1129
1130    /**
1131     * Sets the Lucene analyzer used for this index.<p>
1132     *
1133     * @param analyzer the Lucene analyzer to set
1134     */
1135    public void setAnalyzer(Analyzer analyzer) {
1136
1137        m_analyzer = analyzer;
1138    }
1139
1140    /**
1141     * Sets the checkPermissions.<p>
1142     *
1143     * @param checkPermissions the checkPermissions to set
1144     */
1145    public void setCheckPermissions(boolean checkPermissions) {
1146
1147        m_checkPermissions = checkPermissions;
1148    }
1149
1150    /**
1151     * Sets the ignoreExpiration.<p>
1152     *
1153     * @param ignoreExpiration the ignoreExpiration to set
1154     */
1155    public void setIgnoreExpiration(boolean ignoreExpiration) {
1156
1157        m_ignoreExpiration = ignoreExpiration;
1158    }
1159
1160    /**
1161     * Sets the number of how many hits are loaded at maximum.<p>
1162     *
1163     * This must be set at least to 50, or this setting is ignored.<p>
1164     *
1165     * @param maxHits the number of how many hits are loaded at maximum to set
1166     *
1167     * @see #getMaxHits()
1168     *
1169     * @since 7.5.1
1170     */
1171    public void setMaxHits(int maxHits) {
1172
1173        if (m_maxHits >= (MAX_HITS_DEFAULT / 100)) {
1174            m_maxHits = maxHits;
1175        }
1176    }
1177
1178    /**
1179     * Controls if a resource requires view permission to be displayed in the result list.<p>
1180     *
1181     * By default this is <code>false</code>.<p>
1182     *
1183     * @param requireViewPermission controls if a resource requires view permission to be displayed in the result list
1184     */
1185    public void setRequireViewPermission(boolean requireViewPermission) {
1186
1187        m_requireViewPermission = requireViewPermission;
1188    }
1189
1190    /**
1191     * Shuts down the search index.<p>
1192     *
1193     * This will close the local Lucene index searcher instance.<p>
1194     */
1195    @Override
1196    public void shutDown() {
1197
1198        super.shutDown();
1199        indexSearcherClose();
1200        if (m_analyzer != null) {
1201            m_analyzer.close();
1202        }
1203        if (CmsLog.INIT.isInfoEnabled()) {
1204            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_SHUTDOWN_INDEX_1, getName()));
1205        }
1206    }
1207
1208    /**
1209     * Returns the name (<code>{@link #getName()}</code>) of this search index.<p>
1210     *
1211     * @return the name (<code>{@link #getName()}</code>) of this search index
1212     *
1213     * @see java.lang.Object#toString()
1214     */
1215    @Override
1216    public String toString() {
1217
1218        return getName();
1219    }
1220
1221    /**
1222     * Appends the a category filter to the given filter clause that matches all given categories.<p>
1223     *
1224     * In case the provided List is null or empty, the original filter is left unchanged.<p>
1225     *
1226     * The original filter parameter is extended and also provided as return value.<p>
1227     *
1228     * @param cms the current OpenCms search context
1229     * @param filter the filter to extend
1230     * @param categories the categories that will compose the filter
1231     *
1232     * @return the extended filter clause
1233     */
1234    protected BooleanQuery.Builder appendCategoryFilter(
1235        CmsObject cms,
1236        BooleanQuery.Builder filter,
1237        List<String> categories) {
1238
1239        if ((categories != null) && (categories.size() > 0)) {
1240            // add query categories (if required)
1241
1242            // categories are indexed as lower-case strings
1243            // @see org.opencms.search.fields.CmsSearchFieldConfiguration#appendCategories
1244            List<String> lowerCaseCategories = new ArrayList<String>();
1245            for (String category : categories) {
1246                lowerCaseCategories.add(category.toLowerCase());
1247            }
1248            filter.add(
1249                new BooleanClause(
1250                    getMultiTermQueryFilter(CmsSearchField.FIELD_CATEGORY, lowerCaseCategories),
1251                    BooleanClause.Occur.MUST));
1252        }
1253
1254        return filter;
1255    }
1256
1257    /**
1258     * Appends a date of creation filter to the given filter clause that matches the
1259     * given time range.<p>
1260     *
1261     * If the start time is equal to {@link Long#MIN_VALUE} and the end time is equal to {@link Long#MAX_VALUE}
1262     * than the original filter is left unchanged.<p>
1263     *
1264     * The original filter parameter is extended and also provided as return value.<p>
1265     *
1266     * @param filter the filter to extend
1267     * @param startTime start time of the range to search in
1268     * @param endTime end time of the range to search in
1269     *
1270     * @return the extended filter clause
1271     */
1272    protected BooleanQuery.Builder appendDateCreatedFilter(BooleanQuery.Builder filter, long startTime, long endTime) {
1273
1274        // create special optimized sub-filter for the date last modified search
1275        Query dateFilter = createDateRangeFilter(CmsSearchField.FIELD_DATE_CREATED_LOOKUP, startTime, endTime);
1276        if (dateFilter != null) {
1277            // extend main filter with the created date filter
1278            filter.add(new BooleanClause(dateFilter, BooleanClause.Occur.MUST));
1279        }
1280
1281        return filter;
1282    }
1283
1284    /**
1285     * Appends a date of last modification filter to the given filter clause that matches the
1286     * given time range.<p>
1287     *
1288     * If the start time is equal to {@link Long#MIN_VALUE} and the end time is equal to {@link Long#MAX_VALUE}
1289     * than the original filter is left unchanged.<p>
1290     *
1291     * The original filter parameter is extended and also provided as return value.<p>
1292     *
1293     * @param filter the filter to extend
1294     * @param startTime start time of the range to search in
1295     * @param endTime end time of the range to search in
1296     *
1297     * @return the extended filter clause
1298     */
1299    protected BooleanQuery.Builder appendDateLastModifiedFilter(
1300        BooleanQuery.Builder filter,
1301        long startTime,
1302        long endTime) {
1303
1304        // create special optimized sub-filter for the date last modified search
1305        Query dateFilter = createDateRangeFilter(CmsSearchField.FIELD_DATE_LASTMODIFIED_LOOKUP, startTime, endTime);
1306        if (dateFilter != null) {
1307            // extend main filter with the created date filter
1308            filter.add(new BooleanClause(dateFilter, BooleanClause.Occur.MUST));
1309        }
1310
1311        return filter;
1312    }
1313
1314    /**
1315     * Appends the a VFS path filter to the given filter clause that matches all given root paths.<p>
1316     *
1317     * In case the provided List is null or empty, the current request context site root is appended.<p>
1318     *
1319     * The original filter parameter is extended and also provided as return value.<p>
1320     *
1321     * @param cms the current OpenCms search context
1322     * @param filter the filter to extend
1323     * @param roots the VFS root paths that will compose the filter
1324     *
1325     * @return the extended filter clause
1326     */
1327    protected BooleanQuery.Builder appendPathFilter(CmsObject cms, BooleanQuery.Builder filter, List<String> roots) {
1328
1329        // complete the search root
1330        List<Term> terms = new ArrayList<Term>();
1331        if ((roots != null) && (roots.size() > 0)) {
1332            // add the all configured search roots with will request context
1333            for (int i = 0; i < roots.size(); i++) {
1334                String searchRoot = cms.getRequestContext().addSiteRoot(roots.get(i));
1335                extendPathFilter(terms, searchRoot);
1336            }
1337        } else {
1338            // use the current site root as the search root
1339            extendPathFilter(terms, cms.getRequestContext().getSiteRoot());
1340            // also add the shared folder (v 8.0)
1341            if (OpenCms.getSiteManager().getSharedFolder() != null) {
1342                extendPathFilter(terms, OpenCms.getSiteManager().getSharedFolder());
1343            }
1344        }
1345
1346        // add the calculated path filter for the root path
1347        BooleanQuery.Builder build = new BooleanQuery.Builder();
1348        terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD));
1349        filter.add(new BooleanClause(build.build(), BooleanClause.Occur.MUST));
1350        return filter;
1351    }
1352
1353    /**
1354     * Appends the a resource type filter to the given filter clause that matches all given resource types.<p>
1355     *
1356     * In case the provided List is null or empty, the original filter is left unchanged.<p>
1357     *
1358     * The original filter parameter is extended and also provided as return value.<p>
1359     *
1360     * @param cms the current OpenCms search context
1361     * @param filter the filter to extend
1362     * @param resourceTypes the resource types that will compose the filter
1363     *
1364     * @return the extended filter clause
1365     */
1366    protected BooleanQuery.Builder appendResourceTypeFilter(
1367        CmsObject cms,
1368        BooleanQuery.Builder filter,
1369        List<String> resourceTypes) {
1370
1371        if ((resourceTypes != null) && (resourceTypes.size() > 0)) {
1372            // add query resource types (if required)
1373            filter.add(
1374                new BooleanClause(
1375                    getMultiTermQueryFilter(CmsSearchField.FIELD_TYPE, resourceTypes),
1376                    BooleanClause.Occur.MUST));
1377        }
1378
1379        return filter;
1380    }
1381
1382    /**
1383     * Creates an optimized date range filter for the date of last modification or creation.<p>
1384     *
1385     * If the start date is equal to {@link Long#MIN_VALUE} and the end date is equal to {@link Long#MAX_VALUE}
1386     * than <code>null</code> is returned.<p>
1387     *
1388     * @param fieldName the name of the field to search
1389     * @param startTime start time of the range to search in
1390     * @param endTime end time of the range to search in
1391     *
1392     * @return an optimized date range filter for the date of last modification or creation
1393     */
1394    protected Query createDateRangeFilter(String fieldName, long startTime, long endTime) {
1395
1396        Query filter = null;
1397        if ((startTime != Long.MIN_VALUE) || (endTime != Long.MAX_VALUE)) {
1398            // a date range has been set for this document search
1399            if (startTime == Long.MIN_VALUE) {
1400                // default start will always be "yyyy1231" in order to reduce term size
1401                Calendar cal = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone());
1402                cal.setTimeInMillis(endTime);
1403                cal.set(cal.get(Calendar.YEAR) - MAX_YEAR_RANGE, 11, 31, 0, 0, 0);
1404                startTime = cal.getTimeInMillis();
1405            } else if (endTime == Long.MAX_VALUE) {
1406                // default end will always be "yyyy0101" in order to reduce term size
1407                Calendar cal = Calendar.getInstance(OpenCms.getLocaleManager().getTimeZone());
1408                cal.setTimeInMillis(startTime);
1409                cal.set(cal.get(Calendar.YEAR) + MAX_YEAR_RANGE, 0, 1, 0, 0, 0);
1410                endTime = cal.getTimeInMillis();
1411            }
1412
1413            // get the list of all possible date range options
1414            List<String> dateRange = getDateRangeSpan(startTime, endTime);
1415            List<Term> terms = new ArrayList<Term>();
1416            for (String range : dateRange) {
1417                terms.add(new Term(fieldName, range));
1418            }
1419            // create the filter for the date
1420            BooleanQuery.Builder build = new BooleanQuery.Builder();
1421            terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD));
1422            filter = build.build();
1423        }
1424        return filter;
1425    }
1426
1427    /**
1428     * Creates a backup of this index for optimized re-indexing of the whole content.<p>
1429     *
1430     * @return the path to the backup folder, or <code>null</code> in case no backup was created
1431     */
1432    protected String createIndexBackup() {
1433
1434        if (!isBackupReindexing()) {
1435            // if no backup is generated we don't need to do anything
1436            return null;
1437        }
1438
1439        // check if the target directory already exists
1440        File file = new File(getPath());
1441        if (!file.exists()) {
1442            // index does not exist yet, so we can't backup it
1443            return null;
1444        }
1445        String backupPath = getPath() + "_backup";
1446        FSDirectory oldDir = null;
1447        FSDirectory newDir = null;
1448        try {
1449            // open file directory for Lucene
1450            oldDir = FSDirectory.open(file.toPath());
1451            newDir = FSDirectory.open(Paths.get(backupPath));
1452            for (String fileName : oldDir.listAll()) {
1453                newDir.copyFrom(oldDir, fileName, fileName, IOContext.DEFAULT);
1454            }
1455        } catch (Exception e) {
1456            LOG.error(
1457                Messages.get().getBundle().key(Messages.LOG_IO_INDEX_BACKUP_CREATE_3, getName(), getPath(), backupPath),
1458                e);
1459            backupPath = null;
1460        } finally {
1461            if (oldDir != null) {
1462                try {
1463                    oldDir.close();
1464                } catch (IOException e) {
1465                    e.printStackTrace();
1466                }
1467            }
1468            if (newDir != null) {
1469                try {
1470                    newDir.close();
1471                } catch (IOException e) {
1472                    e.printStackTrace();
1473                }
1474            }
1475        }
1476        return backupPath;
1477    }
1478
1479    /**
1480     * Creates a new index writer.<p>
1481     *
1482     * @param create if <code>true</code> a whole new index is created, if <code>false</code> an existing index is updated
1483     * @param report the report
1484     *
1485     * @return the created new index writer
1486     *
1487     * @throws CmsIndexException in case the writer could not be created
1488     *
1489     * @see #getIndexWriter(I_CmsReport, boolean)
1490     */
1491    @Override
1492    protected I_CmsIndexWriter createIndexWriter(boolean create, I_CmsReport report) throws CmsIndexException {
1493
1494        IndexWriter indexWriter = null;
1495        FSDirectory dir = null;
1496        try {
1497            File f = new File(getPath());
1498            if (!f.exists()) {
1499                f = f.getParentFile();
1500                if ((f != null) && (!f.exists())) {
1501                    f.mkdirs();
1502                }
1503
1504                create = true;
1505            }
1506
1507            dir = FSDirectory.open(Paths.get(getPath()));
1508            IndexWriterConfig indexConfig = new IndexWriterConfig(getAnalyzer());
1509            //indexConfig.setMergePolicy(mergePolicy);
1510
1511            if (m_luceneRAMBufferSizeMB != null) {
1512                indexConfig.setRAMBufferSizeMB(m_luceneRAMBufferSizeMB.doubleValue());
1513            }
1514            if (create) {
1515                indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
1516            } else {
1517                indexConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
1518            }
1519            // register the modified default similarity implementation
1520            indexConfig.setSimilarity(m_sim);
1521
1522            indexWriter = new IndexWriter(dir, indexConfig);
1523        } catch (Exception e) {
1524            if (dir != null) {
1525                try {
1526                    dir.close();
1527                } catch (IOException e1) {
1528                    // TODO Auto-generated catch block
1529                    e1.printStackTrace();
1530                }
1531            }
1532            if (indexWriter != null) {
1533                try {
1534                    indexWriter.close();
1535                } catch (IOException closeExeception) {
1536                    throw new CmsIndexException(
1537                        Messages.get().container(Messages.ERR_IO_INDEX_WRITER_OPEN_2, getPath(), getName()),
1538                        e);
1539                }
1540            }
1541            throw new CmsIndexException(
1542                Messages.get().container(Messages.ERR_IO_INDEX_WRITER_OPEN_2, getPath(), getName()),
1543                e);
1544        }
1545
1546        return new CmsLuceneIndexWriter(indexWriter, this);
1547    }
1548
1549    /**
1550     * Extends the given path query with another term for the given search root element.<p>
1551     *
1552     * @param terms the path filter to extend
1553     * @param searchRoot the search root to add to the path query
1554     */
1555    protected void extendPathFilter(List<Term> terms, String searchRoot) {
1556
1557        if (!CmsResource.isFolder(searchRoot)) {
1558            searchRoot += "/";
1559        }
1560        terms.add(new Term(CmsSearchField.FIELD_PARENT_FOLDERS, searchRoot));
1561    }
1562
1563    /**
1564     * Generates the directory on the RFS for this index.<p>
1565     *
1566     * @return the directory on the RFS for this index
1567     */
1568    protected String generateIndexDirectory() {
1569
1570        return OpenCms.getSystemInfo().getAbsoluteRfsPathRelativeToWebInf(
1571            OpenCms.getSearchManager().getDirectory() + "/" + getName());
1572    }
1573
1574    /**
1575     * Returns a cached Lucene term query filter for the given field and terms.<p>
1576     *
1577     * @param field the field to use
1578     * @param terms the term to use
1579     *
1580     * @return a cached Lucene term query filter for the given field and terms
1581     */
1582    protected Query getMultiTermQueryFilter(String field, List<String> terms) {
1583
1584        return getMultiTermQueryFilter(field, null, terms);
1585    }
1586
1587    /**
1588     * Returns a cached Lucene term query filter for the given field and terms.<p>
1589     *
1590     * @param field the field to use
1591     * @param terms the term to use
1592     *
1593     * @return a cached Lucene term query filter for the given field and terms
1594     */
1595    protected Query getMultiTermQueryFilter(String field, String terms) {
1596
1597        return getMultiTermQueryFilter(field, terms, null);
1598    }
1599
1600    /**
1601     * Returns a cached Lucene term query filter for the given field and terms.<p>
1602     *
1603     * @param field the field to use
1604     * @param termsStr the terms to use as a String separated by a space ' ' char
1605     * @param termsList the list of terms to use
1606     *
1607     * @return a cached Lucene term query filter for the given field and terms
1608     */
1609    protected Query getMultiTermQueryFilter(String field, String termsStr, List<String> termsList) {
1610
1611        if (termsStr == null) {
1612            StringBuffer buf = new StringBuffer(64);
1613            for (int i = 0; i < termsList.size(); i++) {
1614                if (i > 0) {
1615                    buf.append(' ');
1616                }
1617                buf.append(termsList.get(i));
1618            }
1619            termsStr = buf.toString();
1620        }
1621        Query result = m_displayFilters.get(
1622            (new StringBuffer(64)).append(field).append('|').append(termsStr).toString());
1623        if (result == null) {
1624            List<Term> terms = new ArrayList<Term>();
1625            if (termsList == null) {
1626                termsList = CmsStringUtil.splitAsList(termsStr, ' ');
1627            }
1628            for (int i = 0; i < termsList.size(); i++) {
1629                terms.add(new Term(field, termsList.get(i)));
1630            }
1631
1632            BooleanQuery.Builder build = new BooleanQuery.Builder();
1633            terms.forEach(term -> build.add(new TermQuery(term), Occur.SHOULD));
1634            Query termsQuery = build.build(); //termsFilter
1635
1636            try {
1637                result = termsQuery.createWeight(m_indexSearcher, ScoreMode.COMPLETE_NO_SCORES, 1).getQuery();
1638                m_displayFilters.put(field + termsStr, result);
1639            } catch (IOException e) {
1640                // TODO don't know what happend
1641                e.printStackTrace();
1642            }
1643        }
1644        return result;
1645    }
1646
1647    /**
1648     * Checks if the OpenCms resource referenced by the result document can be read
1649     * by the user of the given OpenCms context.
1650     *
1651     * Returns the referenced <code>CmsResource</code> or <code>null</code> if
1652     * the user is not permitted to read the resource.<p>
1653     *
1654     * @param cms the OpenCms user context to use for permission testing
1655     * @param doc the search result document to check
1656     *
1657     * @return the referenced <code>CmsResource</code> or <code>null</code> if the user is not permitted
1658     */
1659    protected CmsResource getResource(CmsObject cms, I_CmsSearchDocument doc) {
1660
1661        // check if the resource exits in the VFS,
1662        // this will implicitly check read permission and if the resource was deleted
1663        CmsResourceFilter filter = CmsResourceFilter.DEFAULT;
1664        if (isRequireViewPermission()) {
1665            filter = CmsResourceFilter.DEFAULT_ONLY_VISIBLE;
1666        } else if (isIgnoreExpiration()) {
1667            filter = CmsResourceFilter.IGNORE_EXPIRATION;
1668        }
1669
1670        return getResource(cms, doc, filter);
1671    }
1672
1673    /**
1674     * Checks if the OpenCms resource referenced by the result document can be read
1675     * by the user of the given OpenCms context.
1676     *
1677     * Returns the referenced <code>CmsResource</code> or <code>null</code> if
1678     * the user is not permitted to read the resource.<p>
1679     *
1680     * @param cms the OpenCms user context to use for permission testing
1681     * @param doc the search result document to check
1682     * @param filter the resource filter to apply
1683     *
1684     * @return the referenced <code>CmsResource</code> or <code>null</code> if the user is not permitted
1685     */
1686    protected CmsResource getResource(CmsObject cms, I_CmsSearchDocument doc, CmsResourceFilter filter) {
1687
1688        try {
1689            CmsObject clone = OpenCms.initCmsObject(cms);
1690            clone.getRequestContext().setSiteRoot("");
1691            return clone.readResource(doc.getPath(), filter);
1692        } catch (CmsException e) {
1693            // Do nothing
1694        }
1695
1696        return null;
1697    }
1698
1699    /**
1700     * Returns a cached Lucene term query filter for the given field and term.<p>
1701     *
1702     * @param field the field to use
1703     * @param term the term to use
1704     *
1705     * @return a cached Lucene term query filter for the given field and term
1706     */
1707    protected Query getTermQueryFilter(String field, String term) {
1708
1709        return getMultiTermQueryFilter(field, term, Collections.singletonList(term));
1710    }
1711
1712    /**
1713     * Checks if the OpenCms resource referenced by the result document can be read
1714     * be the user of the given OpenCms context.<p>
1715     *
1716     * @param cms the OpenCms user context to use for permission testing
1717     * @param doc the search result document to check
1718     * @return <code>true</code> if the user has read permissions to the resource
1719     */
1720    protected boolean hasReadPermission(CmsObject cms, I_CmsSearchDocument doc) {
1721
1722        // If no permission check is needed: the document can be read
1723        // Else try to read the resource if this is not possible the user does not have enough permissions
1724        return !needsPermissionCheck(doc) ? true : (null != getResource(cms, doc));
1725    }
1726
1727    /**
1728     * Closes the index searcher for this index.<p>
1729     *
1730     * @see #indexSearcherOpen(String)
1731     */
1732    protected synchronized void indexSearcherClose() {
1733
1734        indexSearcherClose(m_indexSearcher);
1735    }
1736
1737    /**
1738     * Closes the given Lucene index searcher.<p>
1739     *
1740     * @param searcher the searcher to close
1741     */
1742    protected synchronized void indexSearcherClose(IndexSearcher searcher) {
1743
1744        // in case there is an index searcher available close it
1745        if ((searcher != null) && (searcher.getIndexReader() != null)) {
1746            try {
1747                searcher.getIndexReader().close();
1748            } catch (Exception e) {
1749                LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_CLOSE_1, getName()), e);
1750            }
1751        }
1752    }
1753
1754    /**
1755     * Initializes the index searcher for this index.<p>
1756     *
1757     * In case there is an index searcher still open, it is closed first.<p>
1758     *
1759     * For performance reasons, one instance of the index searcher should be kept
1760     * for all searches. However, if the index is updated or changed
1761     * this searcher instance needs to be re-initialized.<p>
1762     *
1763     * @param path the path to the index directory
1764     */
1765    protected synchronized void indexSearcherOpen(String path) {
1766
1767        IndexSearcher oldSearcher = null;
1768        Directory indexDirectory = null;
1769        try {
1770            indexDirectory = FSDirectory.open(Paths.get(path));
1771            if (DirectoryReader.indexExists(indexDirectory)) {
1772                IndexReader reader = UninvertingReader.wrap(
1773                    DirectoryReader.open(indexDirectory),
1774                    createUninvertingMap());
1775                if (m_indexSearcher != null) {
1776                    // store old searcher instance to close it later
1777                    oldSearcher = m_indexSearcher;
1778                }
1779                m_indexSearcher = new IndexSearcher(reader);
1780                m_indexSearcher.setSimilarity(m_sim);
1781                m_displayFilters = new HashMap<>();
1782            }
1783        } catch (IOException e) {
1784            LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_1, getName()), e);
1785            if (indexDirectory != null) {
1786                try {
1787                    indexDirectory.close();
1788                } catch (IOException closeException) {
1789                    // do nothing
1790                }
1791            }
1792        }
1793        if (oldSearcher != null) {
1794            // close the old searcher if required
1795            indexSearcherClose(oldSearcher);
1796        }
1797    }
1798
1799    /**
1800     * Reopens the index search reader for this index, required after the index has been changed.<p>
1801     *
1802     * @see #indexSearcherOpen(String)
1803     */
1804    protected synchronized void indexSearcherUpdate() {
1805
1806        IndexSearcher oldSearcher = m_indexSearcher;
1807        if ((oldSearcher != null) && (oldSearcher.getIndexReader() != null)) {
1808            // in case there is an index searcher available close it
1809            try {
1810                if (oldSearcher.getIndexReader() instanceof DirectoryReader) {
1811                    IndexReader newReader = DirectoryReader.openIfChanged(
1812                        (DirectoryReader)oldSearcher.getIndexReader());
1813                    if (newReader != null) {
1814                        m_indexSearcher = new IndexSearcher(newReader);
1815                        m_indexSearcher.setSimilarity(m_sim);
1816                        indexSearcherClose(oldSearcher);
1817                    }
1818                }
1819            } catch (Exception e) {
1820                LOG.error(Messages.get().getBundle().key(Messages.ERR_INDEX_SEARCHER_REOPEN_1, getName()), e);
1821            }
1822        } else {
1823            // make sure we end up with an open index searcher / reader
1824            indexSearcherOpen(getPath());
1825        }
1826    }
1827
1828    /**
1829     * Checks if the document is in the time range specified in the search parameters.<p>
1830     *
1831     * The creation date and/or the last modification date are checked.<p>
1832     *
1833     * @param doc the document to check the dates against the given time range
1834     * @param params the search parameters where the time ranges are specified
1835     *
1836     * @return true if document is in time range or not time range set otherwise false
1837     */
1838    protected boolean isInTimeRange(Document doc, CmsSearchParameters params) {
1839
1840        if (!isCheckingTimeRange()) {
1841            // time range check disabled
1842            return true;
1843        }
1844
1845        try {
1846            // check the creation date of the document against the given time range
1847            Date dateCreated = DateTools.stringToDate(doc.getField(CmsSearchField.FIELD_DATE_CREATED).stringValue());
1848            if (dateCreated.getTime() < params.getMinDateCreated()) {
1849                return false;
1850            }
1851            if (dateCreated.getTime() > params.getMaxDateCreated()) {
1852                return false;
1853            }
1854
1855            // check the last modification date of the document against the given time range
1856            Date dateLastModified = DateTools.stringToDate(
1857                doc.getField(CmsSearchField.FIELD_DATE_LASTMODIFIED).stringValue());
1858            if (dateLastModified.getTime() < params.getMinDateLastModified()) {
1859                return false;
1860            }
1861            if (dateLastModified.getTime() > params.getMaxDateLastModified()) {
1862                return false;
1863            }
1864
1865        } catch (ParseException ex) {
1866            // date could not be parsed -> doc is in time range
1867        }
1868
1869        return true;
1870    }
1871
1872    /**
1873     * Checks if the score for the results must be calculated based on the provided sort option.<p>
1874     *
1875     * Since Lucene 3 apparently the score is no longer calculated by default, but only if the
1876     * searcher is explicitly told so. This methods checks if, based on the given sort,
1877     * the score must be calculated.<p>
1878     *
1879     * @param searcher the index searcher to prepare
1880     * @param sort the sort option to use
1881     *
1882     * @return true if the sort option should be used
1883     */
1884    protected boolean isSortScoring(IndexSearcher searcher, Sort sort) {
1885
1886        boolean doScoring = false;
1887        if (sort != null) {
1888            if ((sort == CmsSearchParameters.SORT_DEFAULT) || (sort == CmsSearchParameters.SORT_TITLE)) {
1889                // these default sorts do need score calculation
1890                doScoring = true;
1891            } else if ((sort == CmsSearchParameters.SORT_DATE_CREATED)
1892                || (sort == CmsSearchParameters.SORT_DATE_LASTMODIFIED)) {
1893                    // these default sorts don't need score calculation
1894                    doScoring = false;
1895                } else {
1896                    // for all non-defaults: check if the score field is present, in that case we must calculate the score
1897                    SortField[] fields = sort.getSort();
1898                    for (SortField field : fields) {
1899                        if (field == SortField.FIELD_SCORE) {
1900                            doScoring = true;
1901                            break;
1902                        }
1903                    }
1904                }
1905        }
1906        return doScoring;
1907    }
1908
1909    /**
1910     * Checks if the OpenCms resource referenced by the result document needs to be checked.<p>
1911     *
1912     * @param doc the search result document to check
1913     *
1914     * @return <code>true</code> if the document needs to be checked <code>false</code> otherwise
1915     */
1916    protected boolean needsPermissionCheck(I_CmsSearchDocument doc) {
1917
1918        if (!isCheckingPermissions()) {
1919            // no permission check is performed at all
1920            return false;
1921        }
1922
1923        if ((doc.getType() == null) || (doc.getPath() == null)) {
1924            // permission check needs only to be performed for VFS documents that contain both fields
1925            return false;
1926        }
1927
1928        if (!I_CmsSearchDocument.VFS_DOCUMENT_KEY_PREFIX.equals(doc.getType())
1929            && !OpenCms.getResourceManager().hasResourceType(doc.getType())) {
1930            // this is an unknown VFS resource type (also not the generic "VFS" type of OpenCms before 7.0)
1931            return false;
1932        }
1933        return true;
1934    }
1935
1936    /**
1937     * Removes the given backup folder of this index.<p>
1938     *
1939     * @param path the backup folder to remove
1940     */
1941    protected void removeIndexBackup(String path) {
1942
1943        if (!isBackupReindexing()) {
1944            // if no backup is generated we don't need to do anything
1945            return;
1946        }
1947
1948        // check if the target directory already exists
1949        File file = new File(path);
1950        if (!file.exists()) {
1951            // index does not exist yet
1952            return;
1953        }
1954        try {
1955            FSDirectory dir = FSDirectory.open(file.toPath());
1956            dir.close();
1957            CmsFileUtil.purgeDirectory(file);
1958        } catch (Exception e) {
1959            LOG.error(Messages.get().getBundle().key(Messages.LOG_IO_INDEX_BACKUP_REMOVE_2, getName(), path), e);
1960        }
1961    }
1962
1963    /**
1964     * Generates the uninverting map and adds it to the field configuration.
1965     * @return the generated uninverting map
1966     *
1967     * @see CmsSearchField#addUninvertingMappings(Map)
1968     */
1969    private Map<String, Type> createUninvertingMap() {
1970
1971        Map<String, UninvertingReader.Type> uninvertingMap = new HashMap<String, UninvertingReader.Type>();
1972        CmsSearchField.addUninvertingMappings(uninvertingMap);
1973        getFieldConfiguration().addUninvertingMappings(uninvertingMap);
1974        return uninvertingMap;
1975    }
1976
1977}