org.elasticsearch.index.mapper.hashsplitter.HashSplitterFieldMapper.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticsearch.index.mapper.hashsplitter.HashSplitterFieldMapper.java

Source

/*
 * Licensed to Elastic Search and Shay Banon under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Elastic Search licenses this
 * file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.index.mapper.hashsplitter;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanFilter;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.MatchNoDocsFilter;
import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.search.PrefixFilter;
import org.apache.lucene.search.PrefixLengthFilter;
import org.apache.lucene.search.PrefixLengthQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeLengthFilter;
import org.apache.lucene.search.WildcardFilter;
import org.apache.lucene.search.WildcardQuery;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.FastStringReader;
import org.elasticsearch.common.lucene.search.TermFilter;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.analysis.HashSplitterAnalyzer;
import org.elasticsearch.index.analysis.HashSplitterSearchAnalyzer;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.index.mapper.MapperParsingException;
import org.elasticsearch.index.mapper.MergeContext;
import org.elasticsearch.index.mapper.MergeMappingException;
import org.elasticsearch.index.mapper.core.AbstractFieldMapper;
import org.elasticsearch.index.mapper.core.StringFieldMapper;
import org.elasticsearch.index.query.QueryParseContext;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeIntegerValue;
import static org.elasticsearch.index.mapper.core.TypeParsers.parseField;

public class HashSplitterFieldMapper extends StringFieldMapper implements CustomWildcardSearchFieldMapper {

    public static final String CONTENT_TYPE = "hashsplitter";

    public static HashSplitterFieldMapper.Builder hashSplitterField(String name) {
        return new HashSplitterFieldMapper.Builder(name);
    }

    public static class Defaults {
        public static final Boolean INCLUDE_IN_ALL = null;
        public static final Field.Index INDEX = Field.Index.ANALYZED;
        public static final Field.Store STORE = Field.Store.NO;
        public static final int CHUNK_LENGTH = 1;
        public static final String PREFIXES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.";
        public static final Integer SIZE = null;
        public static final char WILDCARD_ONE = WildcardQuery.DEFAULT_WILDCARD_ONE;
        public static final char WILDCARD_ANY = WildcardQuery.DEFAULT_WILDCARD_ANY;
    }

    public static class Builder extends AbstractFieldMapper.Builder<Builder, HashSplitterFieldMapper> {

        protected String nullValue = StringFieldMapper.Defaults.NULL_VALUE;

        private int chunkLength = Defaults.CHUNK_LENGTH;

        private String prefixes = Defaults.PREFIXES;

        private Integer size = Defaults.SIZE;

        private char wildcardOne = Defaults.WILDCARD_ONE;

        private char wildcardAny = Defaults.WILDCARD_ANY;

        public Builder(String name) {
            super(name);
            super.includeInAll(Defaults.INCLUDE_IN_ALL);
            super.index(Defaults.INDEX);
            super.store(Defaults.STORE);
            builder = this;
        }

        @Override
        public Builder index(Field.Index index) {
            return super.index(index);
        }

        @Override
        public Builder store(Field.Store store) {
            return super.store(store);
        }

        @Override
        public Builder boost(float boost) {
            return super.boost(boost);
        }

        @Override
        public Builder indexName(String indexName) {
            return super.indexName(indexName);
        }

        public Builder nullValue(String nullValue) {
            this.nullValue = nullValue;
            return this;
        }

        @Override
        public Builder includeInAll(Boolean includeInAll) {
            this.includeInAll = includeInAll;
            return this;
        }

        public Builder chunkLength(int chunkLen) {
            this.chunkLength = chunkLen;
            return this;
        }

        public Builder prefixes(String prefixes) {
            this.prefixes = prefixes;
            return this;
        }

        public Builder size(Integer size) {
            this.size = size;
            return this;
        }

        public Builder wildcardOne(char wildcardOne) {
            this.wildcardOne = wildcardOne;
            return this;
        }

        public Builder wildcardAny(char wildcardAny) {
            this.wildcardAny = wildcardAny;
            return this;
        }

        @Override
        public HashSplitterFieldMapper build(BuilderContext context) {
            HashSplitterFieldMapper fieldMapper = new HashSplitterFieldMapper(buildNames(context), index, store,
                    boost, nullValue, chunkLength, prefixes, size == null, size == null ? -1 : size.intValue(),
                    wildcardOne, wildcardAny);
            fieldMapper.includeInAll(includeInAll);
            return fieldMapper;
        }
    }

    /**
     * <pre>
     *  field1 : { type : "hashsplitter" }
     * </pre>
     * Or:
     * <pre>
     *  field1 : {
     *      type : "hashsplitter",
     *      settings: {
     *          chunk_length : 1,
     *          prefixes : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,."
     *          size : "variable" | 32,
     *          wildcard_one : "?",
     *          wildcard_any : "*"
     *      }
     * }
     * </pre>
     */
    public static class TypeParser implements Mapper.TypeParser {
        @Override
        public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext)
                throws MapperParsingException {
            HashSplitterFieldMapper.Builder builder = hashSplitterField(name);
            parseField(builder, name, node, parserContext);
            for (Map.Entry<String, Object> entry : node.entrySet()) {
                String fieldName = Strings.toUnderscoreCase(entry.getKey());
                Object fieldNode = entry.getValue();
                if (fieldName.equals("null_value")) {
                    builder.nullValue(fieldNode.toString());
                } else if (fieldName.equals("settings")) {
                    Map<String, Object> fieldsNode = (Map<String, Object>) fieldNode;
                    for (Map.Entry<String, Object> entry1 : fieldsNode.entrySet()) {
                        String propName = entry1.getKey();
                        Object propNode = entry1.getValue();

                        if ("chunk_length".equals(propName)) {
                            builder.chunkLength(nodeIntegerValue(propNode));
                        } else if ("prefixes".equals(propName)) {
                            builder.prefixes(propNode.toString());
                        } else if ("size".equals(propName)) {
                            builder.size(nodeSizeValue(propNode));
                        } else if ("wildcard_one".equals(propName)) {
                            String value = propNode.toString();
                            if (value.length() != 1)
                                throw new MapperParsingException("[" + HashSplitterFieldMapper.CONTENT_TYPE
                                        + "] Field " + name + " only supports 1-character long wildcard_one");
                            builder.wildcardOne(value.charAt(0));
                        } else if ("wildcard_any".equals(propName)) {
                            String value = propNode.toString();
                            if (value.length() != 1)
                                throw new MapperParsingException("[" + HashSplitterFieldMapper.CONTENT_TYPE
                                        + "] Field " + name + " only supports 1-character long wildcard_any");
                            builder.wildcardAny(value.charAt(0));
                        }
                    }
                }
            }
            return builder;
        }

        public static Integer nodeSizeValue(Object node) {
            if (node instanceof Number) {
                if (node instanceof Integer)
                    return (Integer) node;
                return ((Number) node).intValue();
            }
            try {
                return Integer.valueOf(node.toString());
            } catch (NumberFormatException ex) {
                return null;
            }
        }

    }

    protected String nullValue;

    protected Boolean includeInAll;

    protected int chunkLength;

    protected String prefixes;

    protected boolean sizeIsVariable;

    protected int sizeValue;

    protected char wildcardOne;

    protected char wildcardAny;

    protected HashSplitterAnalyzer indexAnalyzer;

    protected HashSplitterSearchAnalyzer searchAnalyzer;

    public HashSplitterFieldMapper(Names names, Field.Index index, Field.Store store, float boost, String nullValue,
            int chunkLength, String prefixes, boolean sizeIsVariable, int sizeValue, char wildcardOne,
            char wildcardAny) {
        super(names, index, store, Field.TermVector.NO, boost, true, true, nullValue, null, null);
        this.nullValue = nullValue;
        this.includeInAll = null;
        this.chunkLength = chunkLength;
        this.prefixes = prefixes;
        this.sizeIsVariable = sizeIsVariable;
        this.sizeValue = sizeValue;
        this.wildcardOne = wildcardOne;
        this.wildcardAny = wildcardAny;
        this.indexAnalyzer = new HashSplitterAnalyzer(this.chunkLength, this.prefixes);
        this.searchAnalyzer = new HashSplitterSearchAnalyzer(this.chunkLength, this.prefixes, this.wildcardOne,
                this.wildcardAny, this.sizeIsVariable, this.sizeValue);
    }

    @Override
    public void includeInAll(Boolean includeInAll) {
        if (includeInAll != null) {
            this.includeInAll = includeInAll;
        }
        // Keep underlying, private includeInAll field in sync
        super.includeInAll(includeInAll);
    }

    @Override
    public void includeInAllIfNotSet(Boolean includeInAll) {
        if (includeInAll != null && this.includeInAll == null) {
            this.includeInAll = includeInAll;
        }
        // Keep underlying, private includeInAll field in sync
        super.includeInAllIfNotSet(includeInAll);
    }

    @Override
    public void merge(Mapper mergeWith, MergeContext mergeContext) throws MergeMappingException {
        super.merge(mergeWith, mergeContext);
        if (!this.getClass().equals(mergeWith.getClass())) {
            return;
        }
        if (!mergeContext.mergeFlags().simulate()) {
            HashSplitterFieldMapper casted = (HashSplitterFieldMapper) mergeWith;
            this.includeInAll = casted.includeInAll;
            this.nullValue = casted.nullValue;
            this.chunkLength = casted.chunkLength;
            this.prefixes = casted.prefixes;
            this.sizeIsVariable = casted.sizeIsVariable;
            this.sizeValue = casted.sizeValue;
            this.wildcardOne = casted.wildcardOne;
            this.wildcardAny = casted.wildcardAny;
            this.indexAnalyzer = new HashSplitterAnalyzer(this.chunkLength, this.prefixes);
            this.searchAnalyzer = new HashSplitterSearchAnalyzer(this.chunkLength, this.prefixes, this.wildcardOne,
                    this.wildcardAny, this.sizeIsVariable, this.sizeValue);
        }
    }

    @Override
    protected String contentType() {
        return CONTENT_TYPE;
    }

    @Override
    protected void doXContentBody(XContentBuilder builder) throws IOException {
        // As we change the default values for many fields,
        // it may be more interesting not to inherit StringFieldMapper but AbstractFieldMapper directly,
        //super.doXContentBody(builder);

        // From AbstractFieldMapper
        builder.field("type", contentType());
        if (!names.name().equals(names.indexNameClean())) {
            builder.field("index_name", names.indexNameClean());
        }
        if (boost != 1.0f) {
            builder.field("boost", boost);
        }
        // From StringFieldMapper
        if (index != Defaults.INDEX) {
            builder.field("index", index.name().toLowerCase());
        }
        if (store != Defaults.STORE) {
            builder.field("store", store.name().toLowerCase());
        }
        if (nullValue != null) {
            builder.field("null_value", nullValue);
        }
        if (includeInAll != null) {
            builder.field("include_in_all", includeInAll);
        }

        // Additions
        if (chunkLength != Defaults.CHUNK_LENGTH || sizeIsVariable != (Defaults.SIZE == null)
                || Defaults.SIZE != null && sizeValue != Defaults.SIZE.intValue()
                || wildcardOne != Defaults.WILDCARD_ONE || wildcardAny != Defaults.WILDCARD_ANY) {
            builder.startObject("settings");
            if (chunkLength != Defaults.CHUNK_LENGTH) {
                builder.field("chunk_length", chunkLength);
            }
            if (prefixes != Defaults.PREFIXES) {
                builder.field("prefixes", prefixes);
            }
            if (sizeIsVariable != (Defaults.SIZE == null)
                    || Defaults.SIZE != null && sizeValue != Defaults.SIZE.intValue()) {
                builder.field("size");
                if (sizeIsVariable)
                    builder.value("variable");
                else
                    builder.value(sizeValue);
            }
            if (wildcardOne != Defaults.WILDCARD_ONE) {
                builder.field("wildcard_one", Character.toString(wildcardOne));
            }
            if (wildcardAny != Defaults.WILDCARD_ANY) {
                builder.field("wildcard_any", Character.toString(wildcardAny));
            }
            builder.endObject();
        }
    }

    @Override
    public Analyzer indexAnalyzer() {
        return indexAnalyzer;
    }

    @Override
    public Analyzer searchAnalyzer() {
        return searchAnalyzer;
    }

    @Override
    public boolean useFieldQueryWithQueryString() {
        // Don't use our search analyzer with all query strings, prefer our overloaded fieldQuery()
        // (See elasticsearch org.apache.lucene.queryParser.MapperQueryParser.getFieldQuery()'s call to this function.)
        return true;
    }

    @Override
    public Query fieldQuery(String value, @Nullable QueryParseContext context) {
        // Use HashSplitterSearch* analysis and post-process it to create the real query
        TokenStream tok = null;
        try {
            tok = indexAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(value));
            tok.reset();
        } catch (IOException e) {
            return null;
        }
        CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
        BooleanQuery q = new BooleanQuery();
        try {
            while (tok.incrementToken()) {
                Term term = names().createIndexNameTerm(termAtt.toString());
                q.add(new TermQuery(term), BooleanClause.Occur.MUST);
            }
            tok.end();
            tok.close();
        } catch (IOException e) {
            e.printStackTrace();
            q = null;
        }
        return q;
    }

    @Override
    public Filter fieldFilter(String value, @Nullable QueryParseContext context) {
        // Use HashSplitterSearch* analysis and post-process it to create the real query
        TokenStream tok = null;
        try {
            tok = indexAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(value));
            tok.reset();
        } catch (IOException e) {
            return null;
        }
        CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
        BooleanFilter f = new BooleanFilter();
        try {
            while (tok.incrementToken()) {
                Term term = names().createIndexNameTerm(termAtt.toString());
                f.add(new TermFilter(term), BooleanClause.Occur.MUST);
            }
            tok.end();
            tok.close();
        } catch (IOException e) {
            e.printStackTrace();
            f = null;
        }
        return f;
    }

    @Override
    public Query prefixQuery(String value, @Nullable MultiTermQuery.RewriteMethod method,
            @Nullable QueryParseContext context) {
        // Use HashSplitterSearch* analysis and post-process it to create the real query
        TokenStream tok = null;
        try {
            tok = indexAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(value));
            tok.reset();
        } catch (IOException e) {
            return null;
        }
        CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
        BooleanQuery q = new BooleanQuery();
        try {
            int remainingSize = sizeIsVariable ? 0 : sizeValue; // note: prefixes are not included
            while (tok.incrementToken()) {
                Term term = names().createIndexNameTerm(termAtt.toString());
                if (termAtt.length() < 1 + chunkLength) {
                    if (remainingSize > 0) { // implies size is fixed
                        if (remainingSize < chunkLength)
                            q.add(new PrefixLengthQuery(term, 1 + remainingSize, 1 + remainingSize),
                                    BooleanClause.Occur.MUST);
                        else
                            q.add(new PrefixLengthQuery(term, 1 + chunkLength, 1 + chunkLength),
                                    BooleanClause.Occur.MUST);
                    } else { // varying size: only limit to the chunkLength
                        q.add(new PrefixLengthQuery(term, 0, 1 + chunkLength), BooleanClause.Occur.MUST);
                    }
                } else {
                    q.add(new TermQuery(term), BooleanClause.Occur.MUST);
                }
                remainingSize -= termAtt.length() - 1; // termAtt contains the prefix, remainingSize doesn't take it into account
            }
            tok.end();
            tok.close();
        } catch (IOException e) {
            e.printStackTrace();
            q = null;
        }
        return q;
    }

    @Override
    public Filter prefixFilter(String value, @Nullable QueryParseContext context) {
        // Use HashSplitterSearch* analysis and post-process it to create the real filter
        TokenStream tok = null;
        try {
            tok = indexAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(value));
            tok.reset();
        } catch (IOException e) {
            return null;
        }
        CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
        BooleanFilter f = new BooleanFilter();
        try {
            int remainingSize = sizeIsVariable ? 0 : sizeValue; // note: prefixes are not included
            while (tok.incrementToken()) {
                Term term = names().createIndexNameTerm(termAtt.toString());
                if (termAtt.length() < 1 + chunkLength) {
                    if (remainingSize > 0) { // implies size is fixed
                        if (remainingSize < chunkLength)
                            f.add(new PrefixLengthFilter(term, 1 + remainingSize, 1 + remainingSize),
                                    BooleanClause.Occur.MUST);
                        else
                            f.add(new PrefixLengthFilter(term, 1 + chunkLength, 1 + chunkLength),
                                    BooleanClause.Occur.MUST);
                    } else { // varying size: only limit to the chunkLength
                        f.add(new PrefixLengthFilter(term, 0, 1 + chunkLength), BooleanClause.Occur.MUST);
                    }
                } else {
                    f.add(new TermFilter(term), BooleanClause.Occur.MUST);
                }
                remainingSize -= termAtt.length() - 1; // termAtt contains the prefix, remainingSize doesn't take it into account
            }
            tok.end();
            tok.close();
        } catch (IOException e) {
            e.printStackTrace();
            f = null;
        }
        return f;
    }

    @Override
    public Query rangeQuery(String lowerTerm, String upperTerm, boolean includeLower, boolean includeUpper,
            @Nullable QueryParseContext context) {
        // Delegate to rangeFilter(), and wrap it into a query
        Filter filter = rangeFilter(lowerTerm, upperTerm, includeLower, includeUpper, context);
        if (filter == null)
            return null;
        return new ConstantScoreQuery(filter);
    }

    @Override
    public Filter rangeFilter(String lowerTerm, String upperTerm, boolean includeLower, boolean includeUpper,
            @Nullable QueryParseContext context) {
        // Special case: -infinity to +infinity
        if (lowerTerm == null && upperTerm == null) {
            if (sizeIsVariable)
                return null;
            StringBuilder sbWildcardPart = new StringBuilder();
            for (int i = 0; i < chunkLength; i++)
                sbWildcardPart.append(wildcardOne);
            String wildcardPart = sbWildcardPart.toString();
            BooleanFilter filter = new BooleanFilter();
            for (int i = sizeValue / chunkLength - 1; i >= 0; i--) {
                filter.add(new WildcardFilter(names().createIndexNameTerm(prefixes.charAt(i) + wildcardPart)),
                        BooleanClause.Occur.MUST);
            }
            if (sizeValue % chunkLength != 0) {
                // If the size is not dividible by chunkLength,
                // we still have a last chunk, but that has a shorter length
                filter.add(
                        new WildcardFilter(names().createIndexNameTerm(prefixes.charAt(sizeValue / chunkLength + 1)
                                + wildcardPart.substring(0, sizeValue % chunkLength))),
                        BooleanClause.Occur.MUST);
            }
            return filter;
        }
        // Check for emptyness
        if (lowerTerm != null && upperTerm != null) {
            int cmp = lowerTerm.compareTo(upperTerm);
            // Bound invertion
            if (cmp > 0)
                return MatchNoDocsFilter.INSTANCE;
            // Equal bounds
            if (cmp == 0) {
                // and both inclusive bounds: singleton
                if (includeLower && includeUpper) {
                    // Special case: equal terms
                    return fieldFilter(lowerTerm, context);
                }
                // otherwise, empty range
                return MatchNoDocsFilter.INSTANCE;
            }
        }
        // Analyze lower and upper terms
        List<String> lowerTerms = new LinkedList<String>();
        List<String> upperTerms = new LinkedList<String>();
        if (lowerTerm != null) {
            TokenStream tok = null;
            try {
                tok = indexAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(lowerTerm));
                tok.reset();
            } catch (IOException e) {
                return null;
            }
            CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
            try {
                while (tok.incrementToken())
                    lowerTerms.add(termAtt.toString());
                tok.end();
                tok.close();
            } catch (IOException e) {
                return null;
            }
        }
        if (upperTerm != null) {
            TokenStream tok = null;
            try {
                tok = indexAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(upperTerm));
                tok.reset();
            } catch (IOException e) {
                return null;
            }
            CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
            try {
                while (tok.incrementToken())
                    upperTerms.add(termAtt.toString());
                tok.end();
                tok.close();
            } catch (IOException e) {
                return null;
            }
        }
        // Generate the filter
        BooleanFilter topLevelAndFilter = new BooleanFilter();
        Iterator<String> lowers = lowerTerms.iterator();
        Iterator<String> uppers = upperTerms.iterator();
        String currLower = null;
        String currUpper = null;
        int remainingLowerSize = sizeIsVariable ? 0 : sizeValue;
        int remainingUpperSize = sizeIsVariable ? 0 : sizeValue;

        // First, the common prefix
        while (lowers.hasNext() && uppers.hasNext()) {
            currLower = lowers.next();
            currUpper = uppers.next();
            // The last part cannot be part of the prefix
            // because that special case has already been handled
            if (!lowers.hasNext() || !uppers.hasNext())
                break;
            if (!currLower.equals(currUpper))
                break;
            topLevelAndFilter.add(new TermFilter(names().createIndexNameTerm(currLower)), BooleanClause.Occur.MUST);
            remainingLowerSize -= currLower.length() - 1;
            remainingUpperSize -= currUpper.length() - 1;
        }

        String subPrefixLower = currLower;
        BooleanFilter secondLevelOrFilter = new BooleanFilter();
        BooleanFilter lastFilter;
        // Add the range part of the query (secondLevelOrFilter) to the prefix part is already in topLevelAndFilter
        topLevelAndFilter.add(secondLevelOrFilter, BooleanClause.Occur.MUST);
        // We still have secondLevelOrFilter to populate

        lastFilter = new BooleanFilter();
        // Handle the first diverging token of the lowerTerm (if it's not also the last available!)
        if (lowers.hasNext()) {
            lastFilter.add(new TermFilter(names().createIndexNameTerm(currLower)), BooleanClause.Occur.MUST);
            remainingLowerSize -= currLower.length() - 1;
            currLower = lowers.next();
        }
        secondLevelOrFilter.add(lastFilter, BooleanClause.Occur.SHOULD);
        // Then get to the last token of the lowerTerm
        while (lowers.hasNext()) {
            BooleanFilter orFilter = new BooleanFilter();
            lastFilter.add(orFilter, BooleanClause.Occur.MUST);
            orFilter.add(new TermRangeLengthFilter(names().indexName(), currLower, luceneTermUpperBound(currLower),
                    false, false, 1 + chunkLength, 1 + chunkLength), BooleanClause.Occur.SHOULD);
            BooleanFilter nextFilter = new BooleanFilter();
            nextFilter.add(new TermFilter(names().createIndexNameTerm(currLower)), BooleanClause.Occur.MUST);
            orFilter.add(nextFilter, BooleanClause.Occur.SHOULD);
            lastFilter = nextFilter;
            remainingLowerSize -= currLower.length() - 1;
            currLower = lowers.next();
        }
        // Handle the last token of the lowerTerm
        if (remainingLowerSize < 0)
            lastFilter.add(new TermRangeLengthFilter(names().indexName(), currLower,
                    luceneTermUpperBound(currLower), includeLower, false, 0, 1 + chunkLength),
                    BooleanClause.Occur.MUST);
        else if (remainingLowerSize < chunkLength)
            lastFilter.add(
                    new TermRangeLengthFilter(names().indexName(), currLower, luceneTermUpperBound(currLower),
                            includeLower, false, 1 + remainingLowerSize, 1 + remainingLowerSize),
                    BooleanClause.Occur.MUST);
        else
            lastFilter.add(new TermRangeLengthFilter(names().indexName(), currLower,
                    luceneTermUpperBound(currLower), includeLower, false, 1 + chunkLength, 1 + chunkLength),
                    BooleanClause.Occur.MUST);

        // Range from the non prefix part of the lowerTerm to the non prefix part of the upperTerm
        if (remainingUpperSize < 0)
            secondLevelOrFilter.add(new TermRangeLengthFilter(names().indexName(), subPrefixLower, currUpper, false,
                    false, 0, 1 + chunkLength), BooleanClause.Occur.SHOULD);
        else if (remainingUpperSize < chunkLength)
            secondLevelOrFilter.add(new TermRangeLengthFilter(names().indexName(), subPrefixLower, currUpper, false,
                    false, 1 + remainingUpperSize, 1 + remainingUpperSize), BooleanClause.Occur.SHOULD);
        else
            secondLevelOrFilter.add(new TermRangeLengthFilter(names().indexName(), subPrefixLower, currUpper, false,
                    false, 1 + chunkLength, 1 + chunkLength), BooleanClause.Occur.SHOULD);

        lastFilter = new BooleanFilter();
        // Handle the first diverging token of the upperTerm (if it's not also the last available!)
        if (uppers.hasNext()) {
            lastFilter.add(new TermFilter(names().createIndexNameTerm(currUpper)), BooleanClause.Occur.MUST);
            remainingUpperSize -= currUpper.length() - 1;
            currUpper = uppers.next();
        }
        secondLevelOrFilter.add(lastFilter, BooleanClause.Occur.SHOULD);
        // Then get to the last token of the upperTerm
        while (uppers.hasNext()) {
            BooleanFilter orFilter = new BooleanFilter();
            lastFilter.add(orFilter, BooleanClause.Occur.MUST);
            orFilter.add(new TermRangeLengthFilter(names().indexName(), luceneTermLowerBound(currUpper), currUpper,
                    false, false, 1 + chunkLength, 1 + chunkLength), BooleanClause.Occur.SHOULD);
            BooleanFilter nextFilter = new BooleanFilter();
            nextFilter.add(new TermFilter(names().createIndexNameTerm(currUpper)), BooleanClause.Occur.MUST);
            orFilter.add(nextFilter, BooleanClause.Occur.SHOULD);
            lastFilter = nextFilter;
            remainingUpperSize -= currUpper.length() - 1;
            currUpper = uppers.next();
        }
        // Handle the last token of the upperTerm
        if (remainingUpperSize < 0)
            lastFilter.add(new TermRangeLengthFilter(names().indexName(), luceneTermLowerBound(currUpper),
                    currUpper, false, includeUpper, 0, 1 + chunkLength), BooleanClause.Occur.MUST);
        else if (remainingUpperSize < chunkLength)
            lastFilter.add(
                    new TermRangeLengthFilter(names().indexName(), luceneTermLowerBound(currUpper), currUpper,
                            false, includeUpper, 1 + remainingUpperSize, 1 + remainingUpperSize),
                    BooleanClause.Occur.MUST);
        else
            lastFilter.add(new TermRangeLengthFilter(names().indexName(), luceneTermLowerBound(currUpper),
                    currUpper, false, includeUpper, 1 + chunkLength, 1 + chunkLength), BooleanClause.Occur.MUST);

        return topLevelAndFilter;
    }

    private String luceneTermUpperBound(String term) {
        // Lucene terms are ordered lexicographically (by UTF-16 character code)
        // so we should not use:
        //   return Character.toString(prefixes.charAt((prefixes.indexOf(term.charAt(0))+1)%prefixes.length()));
        // that will get the next *prefix* (the prefix for the *lower level*)
        // but merely the next *char*!
        return Character.toString((char) (term.charAt(0) + 1));
    }

    private String luceneTermLowerBound(String term) {
        // Simply the prefix alone
        return term.substring(0, 1);
    }

    @Override
    public Query queryStringTermQuery(Term term) {
        // Direct use of term queries against such a field implies you know what you are doing
        // (ie. the use of prefixes and chunks)
        // We could also decide to call over to fieldquery().
        return null;
    }

    @Override
    public Query fuzzyQuery(String value, String minSim, int prefixLength, int maxExpansions) {
        // Not supported for now
        return null; // will fallback to an unusable default query
    }

    @Override
    public Query fuzzyQuery(String value, double minSim, int prefixLength, int maxExpansions) {
        // Not supported for now
        return null; // will fallback to an unusable default query
    }

    @Override
    public Query wildcardQuery(String value, @Nullable MultiTermQuery.RewriteMethod method,
            @Nullable QueryParseContext context) {
        // Use HashSplitterSearch* analysis and post-process it to create the real query
        TokenStream tok = null;
        try {
            tok = searchAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(value));
            tok.reset();
        } catch (IOException e) {
            return null;
        }
        CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
        BooleanQuery q = new BooleanQuery();
        try {
            while (tok.incrementToken()) {
                q.add(new WildcardQuery(names().createIndexNameTerm(termAtt.toString()), wildcardOne, wildcardAny),
                        BooleanClause.Occur.MUST);
            }
            tok.end();
            tok.close();
        } catch (IOException e) {
            e.printStackTrace();
            q = null;
        }
        return q;
    }

    @Override
    public Filter wildcardFilter(String value, @Nullable QueryParseContext context) {
        // Use HashSplitterSearch* analysis and post-process it to create the real query
        TokenStream tok = null;
        try {
            tok = searchAnalyzer.reusableTokenStream(names().indexNameClean(), new FastStringReader(value));
            tok.reset();
        } catch (IOException e) {
            return null;
        }
        CharTermAttribute termAtt = tok.getAttribute(CharTermAttribute.class);
        BooleanFilter f = new BooleanFilter();
        try {
            while (tok.incrementToken()) {
                f.add(new WildcardFilter(names().createIndexNameTerm(termAtt.toString()), wildcardOne, wildcardAny),
                        BooleanClause.Occur.MUST);
            }
            tok.end();
            tok.close();
        } catch (IOException e) {
            e.printStackTrace();
            f = null;
        }
        return f;
    }

}