ContextSuggester
================ This commit extends the `CompletionSuggester` by context informations. In example such a context informations can be a simple string representing a category reducing the suggestions in order to this category. Three base implementations of these context informations have been setup in this commit. - a Category Context - a Geo Context All the mapping for these context informations are specified within a context field in the completion field that should use this kind of information.
This commit is contained in:
parent
aecadfcc61
commit
81e537bd5e
|
@ -272,4 +272,6 @@ include::suggesters/phrase-suggest.asciidoc[]
|
||||||
|
|
||||||
include::suggesters/completion-suggest.asciidoc[]
|
include::suggesters/completion-suggest.asciidoc[]
|
||||||
|
|
||||||
|
include::suggesters/context-suggest.asciidoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,9 @@ is used as `score`. Also the `text` field uses the `output` of your
|
||||||
indexed suggestion, if configured, otherwise the matched part of the
|
indexed suggestion, if configured, otherwise the matched part of the
|
||||||
`input` field.
|
`input` field.
|
||||||
|
|
||||||
|
NOTE: The completion suggester considers all documents in the index.
|
||||||
|
See <<suggester-context>> for an explanation of how to query a subset of
|
||||||
|
documents instead.
|
||||||
|
|
||||||
[[fuzzy]]
|
[[fuzzy]]
|
||||||
==== Fuzzy queries
|
==== Fuzzy queries
|
||||||
|
|
|
@ -0,0 +1,319 @@
|
||||||
|
[[suggester-context]]
|
||||||
|
== Context Suggester
|
||||||
|
|
||||||
|
The context suggester is an extension to the suggest API of Elasticsearch. Namely the
|
||||||
|
suggester system provides a very fast way of searching documents by handling these
|
||||||
|
entirely in memory. But this special treatenment does not allow the handling of
|
||||||
|
traditional queries and filters, because those would have notable impact on the
|
||||||
|
performance. So the context extension is designed to take so-called context information
|
||||||
|
into account to specify a more accurate way of searching within the suggester system.
|
||||||
|
Instead of using the traditional query and filter system a predefined ``context`` is
|
||||||
|
configured to limit suggestions to a particular subset of suggestions.
|
||||||
|
Such a context is defined by a set of context mappings which can either be a simple
|
||||||
|
*category* or a *geo location*. The information used by the context suggester is
|
||||||
|
configured in the type mapping with the `context` parameter, which lists all of the
|
||||||
|
contexts that need to be specified in each document and in each suggestion request.
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
PUT services/service/_mapping
|
||||||
|
{
|
||||||
|
"service": {
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"suggest_field": {
|
||||||
|
"type": "completion",
|
||||||
|
"context": {
|
||||||
|
"color": { <1>
|
||||||
|
"type": "category",
|
||||||
|
"path": "color_field"
|
||||||
|
"default": ["red", "green", "blue"]
|
||||||
|
},
|
||||||
|
"location": { <2>
|
||||||
|
"type": "geo",
|
||||||
|
"precision": "5m",
|
||||||
|
"neighbors": true,
|
||||||
|
"default": "u33"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
<1> See <<suggester-context-category>>
|
||||||
|
<2> See <<suggester-context-geo>>
|
||||||
|
|
||||||
|
However contexts are specified (as type `category` or `geo`, which are discussed below), each
|
||||||
|
context value generates a new sub-set of documents which can be queried by the completion
|
||||||
|
suggester. All three types accept a `default` parameter which provides a default value to use
|
||||||
|
if the corresponding context value is absent.
|
||||||
|
|
||||||
|
The basic structure of this element is that each field forms a new context and the fieldname
|
||||||
|
is used to reference this context information later on during indexing or querying. All context
|
||||||
|
mappings have the `default` and the `type` option in common. The value of the `default` field
|
||||||
|
is used, when ever no specific is provided for the certain context. Note that a context is
|
||||||
|
defined by at least one value. The `type` option defines the kind of information hold by this
|
||||||
|
context. These type will be explained further in the following sections.
|
||||||
|
|
||||||
|
[[suggester-context-category]]
|
||||||
|
[float]
|
||||||
|
=== Category Context
|
||||||
|
The `category` context allows you to specify one or more categories in the document at index time.
|
||||||
|
The document will be assigned to each named category, which can then be queried later. The category
|
||||||
|
type also allows to specify a field to extract the categories from. The `path` parameter is used to
|
||||||
|
specify this field of the documents that should be used. If the referenced field contains multiple
|
||||||
|
values, all these values will be used as alternative categories.
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== Category Mapping
|
||||||
|
|
||||||
|
The mapping for a category is simply defined by its `default` values. These can either be
|
||||||
|
defined as list of *default* categories:
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
"context": {
|
||||||
|
"color": {
|
||||||
|
"type": "category",
|
||||||
|
"default": ["red", "orange"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
or as a single value
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
"context": {
|
||||||
|
"color": {
|
||||||
|
"type": "category",
|
||||||
|
"default": "red"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
or as reference to another field within the documents indexed:
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
"context": {
|
||||||
|
"color": {
|
||||||
|
"type": "category",
|
||||||
|
"default": "red"
|
||||||
|
"path": "color_field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
in this case the *default* categories will only be used, if the given field does not
|
||||||
|
exist within the document. In the example above the categories are received from a
|
||||||
|
field named `color_field`. If this field does not exist a category *red* is assumed for
|
||||||
|
the context *color*.
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== Indexing category contexts
|
||||||
|
Within a document the category is specified either as an `array` of values, a
|
||||||
|
single value or `null`. A list of values is interpreted as alternative categories. So
|
||||||
|
a document belongs to all the categories defined. If the category is `null` or remains
|
||||||
|
unset the categories will be retrieved from the documents field addressed by the `path`
|
||||||
|
parameter. If this value is not set or the field is missing, the default values of the
|
||||||
|
mapping will be assigned to the context.
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
PUT services/service/1
|
||||||
|
{
|
||||||
|
"name": "knapsack",
|
||||||
|
"suggest_field": {
|
||||||
|
"input": ["knacksack", "backpack", "daypack"],
|
||||||
|
"context": {
|
||||||
|
"color": ["red", "yellow"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== Category Query
|
||||||
|
A query within a category works similar to the configuration. If the value is `null`
|
||||||
|
the mappings default categories will be used. Otherwise the suggestion takes place
|
||||||
|
for all documents that have at least one category in common with the query.
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
POST services/_suggest?pretty'
|
||||||
|
{
|
||||||
|
"suggest" : {
|
||||||
|
"text" : "m",
|
||||||
|
"completion" : {
|
||||||
|
"field" : "suggest_field",
|
||||||
|
"size": 10,
|
||||||
|
"context": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
[[suggester-context-geo]]
|
||||||
|
[float]
|
||||||
|
=== Geo location Context
|
||||||
|
A `geo` context allows you to limit results to those that lie within a certain distance
|
||||||
|
of a specified geolocation. At index time, a lat/long geo point is converted into a
|
||||||
|
geohash of a certain precision, which provides the context.
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== Geo location Mapping
|
||||||
|
The mapping for a geo context accepts four settings:
|
||||||
|
|
||||||
|
[horizontal]
|
||||||
|
`precision`:: This defines the precision of the geohash and can be specified as `5m`, `10km`,
|
||||||
|
or as a raw geohash precision: `1`..`12`. It's also possible to setup multiple
|
||||||
|
precisions by defining a list of precisions: `["5m", "10km"]`
|
||||||
|
(default is a geohash level of 12)
|
||||||
|
`neighbors`:: Geohashes are rectangles, so a geolocation, which in reality is only 1 metre
|
||||||
|
away from the specified point, may fall into the neighbouring rectangle. Set
|
||||||
|
`neighbours` to `true` to include the neighbouring geohashes in the context.
|
||||||
|
(default is *on*)
|
||||||
|
`path`:: Optionally specify a field to use to look up the geopoint.
|
||||||
|
`default`:: The geopoint to use if no geopoint has been specified.
|
||||||
|
|
||||||
|
Since all locations of this mapping are translated into geohashes, each location matches
|
||||||
|
a geohash cell. So some results that lie within the specified range but not in the same
|
||||||
|
cell as the query location will not match. To avoid this the `neighbors` option allows a
|
||||||
|
matching of cells that join the bordering regions of the documents location. This option
|
||||||
|
is turned on by default.
|
||||||
|
If a document or a query doesn't define a location a value to use instead can defined by
|
||||||
|
the `default` option. The value of this option supports all the ways a `geo_point` can be
|
||||||
|
defined. The `path` refers to another field within the document to retrieve the
|
||||||
|
location. If this field contains multiple values, the document will be linked to all these
|
||||||
|
locations.
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
"context": {
|
||||||
|
"location": {
|
||||||
|
"type": "geo",
|
||||||
|
"precision": ["1km", "5m"],
|
||||||
|
"neighbors": true,
|
||||||
|
"path": "pin",
|
||||||
|
"default": {
|
||||||
|
"lat": 0.0,
|
||||||
|
"lon": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== Geo location Config
|
||||||
|
|
||||||
|
Within a document a geo location retrieved from the mapping definition can be overridden
|
||||||
|
by another location. In this case the context mapped to a geo location supports all
|
||||||
|
variants of defining a `geo_point`.
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
PUT services/service/1
|
||||||
|
{
|
||||||
|
"name": "some hotel 1",
|
||||||
|
"suggest_field": {
|
||||||
|
"input": ["my hotel", "this hotel"],
|
||||||
|
"context": {
|
||||||
|
"location": {
|
||||||
|
"lat": 0,
|
||||||
|
"lon": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== Geo location Query
|
||||||
|
|
||||||
|
Like in the configuration, querying with a geo location in context, the geo location
|
||||||
|
query supports all representations of a `geo_point` to define the location. In this
|
||||||
|
simple case all precision values defined in the mapping will be applied to the given
|
||||||
|
location.
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
POST services/_suggest
|
||||||
|
{
|
||||||
|
"suggest" : {
|
||||||
|
"text" : "m",
|
||||||
|
"completion" : {
|
||||||
|
"field" : "suggest_field",
|
||||||
|
"size": 10,
|
||||||
|
"context": {
|
||||||
|
"location": {
|
||||||
|
"lat": 0,
|
||||||
|
"lon": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
But it also possible to set a subset of the precisions set in the mapping, by using the
|
||||||
|
`precision` parameter. Like in the mapping, this parameter is allowed to be set to a
|
||||||
|
single precision value or a list of these.
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
POST services/_suggest
|
||||||
|
{
|
||||||
|
"suggest" : {
|
||||||
|
"text" : "m",
|
||||||
|
"completion" : {
|
||||||
|
"field" : "suggest_field",
|
||||||
|
"size": 10,
|
||||||
|
"context": {
|
||||||
|
"location": {
|
||||||
|
"value": {
|
||||||
|
"lat": 0,
|
||||||
|
"lon": 0
|
||||||
|
},
|
||||||
|
"precision": "1km"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
A special form of the query is definied by an extension of the object representation of
|
||||||
|
the `geo_point`. Using this representation allows to set the `precision` parameter within
|
||||||
|
the location itself:
|
||||||
|
|
||||||
|
[source,js]
|
||||||
|
--------------------------------------------------
|
||||||
|
POST services/_suggest
|
||||||
|
{
|
||||||
|
"suggest" : {
|
||||||
|
"text" : "m",
|
||||||
|
"completion" : {
|
||||||
|
"field" : "suggest_field",
|
||||||
|
"size": 10,
|
||||||
|
"context": {
|
||||||
|
"location": {
|
||||||
|
"lat": 0,
|
||||||
|
"lon": 0,
|
||||||
|
"precision": "1km"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--------------------------------------------------
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.apache.lucene.analysis;
|
||||||
|
|
||||||
|
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
|
||||||
|
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This {@link Analyzer} wraps another analyzer and adds a set of prefixes to the
|
||||||
|
* underlying TokenStream. While these prefixes are iterated the position attribute
|
||||||
|
* will not be incremented. Also each prefix will be separated from the other tokens
|
||||||
|
* by a separator character.
|
||||||
|
* NOTE: The sequence of prefixes needs to be not empty
|
||||||
|
*/
|
||||||
|
public class PrefixAnalyzer extends Analyzer {
|
||||||
|
|
||||||
|
private final char separator;
|
||||||
|
private final Iterable<? extends CharSequence> prefix;
|
||||||
|
private final Analyzer analyzer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link PrefixAnalyzer}. The separator will be set to the DEFAULT_SEPARATOR.
|
||||||
|
*
|
||||||
|
* @param analyzer {@link Analyzer} to wrap
|
||||||
|
* @param prefix Single prefix
|
||||||
|
*/
|
||||||
|
public PrefixAnalyzer(Analyzer analyzer, char separator, CharSequence prefix) {
|
||||||
|
this(analyzer, separator, Collections.singleton(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link PrefixAnalyzer}. The separator will be set to the DEFAULT_SEPARATOR.
|
||||||
|
*
|
||||||
|
* @param analyzer {@link Analyzer} to wrap
|
||||||
|
* @param prefix {@link Iterable} of {@link CharSequence} which keeps all prefixes
|
||||||
|
*/
|
||||||
|
public PrefixAnalyzer(Analyzer analyzer, char separator, Iterable<? extends CharSequence> prefix) {
|
||||||
|
super();
|
||||||
|
this.analyzer = analyzer;
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.separator = separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TokenStreamComponents createComponents(String fieldName, Reader reader) {
|
||||||
|
TokenStreamComponents createComponents = analyzer.createComponents(fieldName, reader);
|
||||||
|
TokenStream stream = new PrefixTokenFilter(createComponents.getTokenStream(), separator, prefix);
|
||||||
|
TokenStreamComponents tsc = new TokenStreamComponents(createComponents.getTokenizer(), stream);
|
||||||
|
return tsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PrefixTokenFilter} wraps a {@link TokenStream} and adds a set
|
||||||
|
* prefixes ahead. The position attribute will not be incremented for the prefixes.
|
||||||
|
*/
|
||||||
|
public static final class PrefixTokenFilter extends TokenFilter {
|
||||||
|
|
||||||
|
private final char separator;
|
||||||
|
private final CharTermAttribute termAttr = addAttribute(CharTermAttribute.class);
|
||||||
|
private final PositionIncrementAttribute posAttr = addAttribute(PositionIncrementAttribute.class);
|
||||||
|
private final Iterable<? extends CharSequence> prefixes;
|
||||||
|
|
||||||
|
private Iterator<? extends CharSequence> currentPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link PrefixTokenFilter}. The separator will be set to the DEFAULT_SEPARATOR.
|
||||||
|
*
|
||||||
|
* @param input {@link TokenStream} to wrap
|
||||||
|
* @param separator Character used separate prefixes from other tokens
|
||||||
|
* @param prefixes {@link Iterable} of {@link CharSequence} which keeps all prefixes
|
||||||
|
*/
|
||||||
|
public PrefixTokenFilter(TokenStream input, char separator, Iterable<? extends CharSequence> prefixes) {
|
||||||
|
super(input);
|
||||||
|
this.prefixes = prefixes;
|
||||||
|
this.currentPrefix = null;
|
||||||
|
this.separator = separator;
|
||||||
|
assert (prefixes != null && prefixes.iterator().hasNext()) : "one or more prefix needed";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean incrementToken() throws IOException {
|
||||||
|
if (currentPrefix != null) {
|
||||||
|
if (!currentPrefix.hasNext()) {
|
||||||
|
return input.incrementToken();
|
||||||
|
} else {
|
||||||
|
posAttr.setPositionIncrement(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentPrefix = prefixes.iterator();
|
||||||
|
termAttr.setEmpty();
|
||||||
|
posAttr.setPositionIncrement(1);
|
||||||
|
assert (currentPrefix.hasNext()) : "one or more prefixes needed";
|
||||||
|
}
|
||||||
|
termAttr.setEmpty();
|
||||||
|
termAttr.append(currentPrefix.next());
|
||||||
|
termAttr.append(separator);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() throws IOException {
|
||||||
|
super.reset();
|
||||||
|
currentPrefix = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,8 +26,6 @@ import org.apache.lucene.search.suggest.InputIterator;
|
||||||
import org.apache.lucene.search.suggest.Lookup;
|
import org.apache.lucene.search.suggest.Lookup;
|
||||||
import org.apache.lucene.search.suggest.Sort;
|
import org.apache.lucene.search.suggest.Sort;
|
||||||
import org.apache.lucene.store.*;
|
import org.apache.lucene.store.*;
|
||||||
import org.apache.lucene.store.DataInput;
|
|
||||||
import org.apache.lucene.store.DataOutput;
|
|
||||||
import org.apache.lucene.util.*;
|
import org.apache.lucene.util.*;
|
||||||
import org.apache.lucene.util.automaton.*;
|
import org.apache.lucene.util.automaton.*;
|
||||||
import org.apache.lucene.util.fst.*;
|
import org.apache.lucene.util.fst.*;
|
||||||
|
@ -36,7 +34,10 @@ import org.apache.lucene.util.fst.PairOutputs.Pair;
|
||||||
import org.apache.lucene.util.fst.Util.MinResult;
|
import org.apache.lucene.util.fst.Util.MinResult;
|
||||||
import org.elasticsearch.common.collect.HppcMaps;
|
import org.elasticsearch.common.collect.HppcMaps;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,6 +169,8 @@ public class XAnalyzingSuggester extends Lookup {
|
||||||
public static final int PAYLOAD_SEP = '\u001F';
|
public static final int PAYLOAD_SEP = '\u001F';
|
||||||
public static final int HOLE_CHARACTER = '\u001E';
|
public static final int HOLE_CHARACTER = '\u001E';
|
||||||
|
|
||||||
|
private final Automaton queryPrefix;
|
||||||
|
|
||||||
/** Whether position holes should appear in the automaton. */
|
/** Whether position holes should appear in the automaton. */
|
||||||
private boolean preservePositionIncrements;
|
private boolean preservePositionIncrements;
|
||||||
|
|
||||||
|
@ -180,7 +183,7 @@ public class XAnalyzingSuggester extends Lookup {
|
||||||
* PRESERVE_SEP, 256, -1)}
|
* PRESERVE_SEP, 256, -1)}
|
||||||
*/
|
*/
|
||||||
public XAnalyzingSuggester(Analyzer analyzer) {
|
public XAnalyzingSuggester(Analyzer analyzer) {
|
||||||
this(analyzer, analyzer, EXACT_FIRST | PRESERVE_SEP, 256, -1, true, null, false, 0, SEP_LABEL, PAYLOAD_SEP, END_BYTE, HOLE_CHARACTER);
|
this(analyzer, null, analyzer, EXACT_FIRST | PRESERVE_SEP, 256, -1, true, null, false, 0, SEP_LABEL, PAYLOAD_SEP, END_BYTE, HOLE_CHARACTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -189,7 +192,7 @@ public class XAnalyzingSuggester extends Lookup {
|
||||||
* PRESERVE_SEP, 256, -1)}
|
* PRESERVE_SEP, 256, -1)}
|
||||||
*/
|
*/
|
||||||
public XAnalyzingSuggester(Analyzer indexAnalyzer, Analyzer queryAnalyzer) {
|
public XAnalyzingSuggester(Analyzer indexAnalyzer, Analyzer queryAnalyzer) {
|
||||||
this(indexAnalyzer, queryAnalyzer, EXACT_FIRST | PRESERVE_SEP, 256, -1, true, null, false, 0, SEP_LABEL, PAYLOAD_SEP, END_BYTE, HOLE_CHARACTER);
|
this(indexAnalyzer, null, queryAnalyzer, EXACT_FIRST | PRESERVE_SEP, 256, -1, true, null, false, 0, SEP_LABEL, PAYLOAD_SEP, END_BYTE, HOLE_CHARACTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,7 +211,7 @@ public class XAnalyzingSuggester extends Lookup {
|
||||||
* to expand from the analyzed form. Set this to -1 for
|
* to expand from the analyzed form. Set this to -1 for
|
||||||
* no limit.
|
* no limit.
|
||||||
*/
|
*/
|
||||||
public XAnalyzingSuggester(Analyzer indexAnalyzer, Analyzer queryAnalyzer, int options, int maxSurfaceFormsPerAnalyzedForm, int maxGraphExpansions,
|
public XAnalyzingSuggester(Analyzer indexAnalyzer, Automaton queryPrefix, Analyzer queryAnalyzer, int options, int maxSurfaceFormsPerAnalyzedForm, int maxGraphExpansions,
|
||||||
boolean preservePositionIncrements, FST<Pair<Long, BytesRef>> fst, boolean hasPayloads, int maxAnalyzedPathsForOneInput,
|
boolean preservePositionIncrements, FST<Pair<Long, BytesRef>> fst, boolean hasPayloads, int maxAnalyzedPathsForOneInput,
|
||||||
int sepLabel, int payloadSep, int endByte, int holeCharacter) {
|
int sepLabel, int payloadSep, int endByte, int holeCharacter) {
|
||||||
// SIMON EDIT: I added fst, hasPayloads and maxAnalyzedPathsForOneInput
|
// SIMON EDIT: I added fst, hasPayloads and maxAnalyzedPathsForOneInput
|
||||||
|
@ -222,6 +225,9 @@ public class XAnalyzingSuggester extends Lookup {
|
||||||
this.exactFirst = (options & EXACT_FIRST) != 0;
|
this.exactFirst = (options & EXACT_FIRST) != 0;
|
||||||
this.preserveSep = (options & PRESERVE_SEP) != 0;
|
this.preserveSep = (options & PRESERVE_SEP) != 0;
|
||||||
|
|
||||||
|
// FLORIAN EDIT: I added <code>queryPrefix</code> for context dependent suggestions
|
||||||
|
this.queryPrefix = queryPrefix;
|
||||||
|
|
||||||
// NOTE: this is just an implementation limitation; if
|
// NOTE: this is just an implementation limitation; if
|
||||||
// somehow this is a problem we could fix it by using
|
// somehow this is a problem we could fix it by using
|
||||||
// more than one byte to disambiguate ... but 256 seems
|
// more than one byte to disambiguate ... but 256 seems
|
||||||
|
@ -298,6 +304,10 @@ public class XAnalyzingSuggester extends Lookup {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Automaton convertAutomaton(Automaton a) {
|
protected Automaton convertAutomaton(Automaton a) {
|
||||||
|
if (queryPrefix != null) {
|
||||||
|
a = Automaton.concatenate(Arrays.asList(queryPrefix, a));
|
||||||
|
BasicOperations.determinize(a);
|
||||||
|
}
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ public final class XFuzzySuggester extends XAnalyzingSuggester {
|
||||||
* Analyzer that will be used for analyzing query text during lookup
|
* Analyzer that will be used for analyzing query text during lookup
|
||||||
*/
|
*/
|
||||||
public XFuzzySuggester(Analyzer indexAnalyzer, Analyzer queryAnalyzer) {
|
public XFuzzySuggester(Analyzer indexAnalyzer, Analyzer queryAnalyzer) {
|
||||||
this(indexAnalyzer, queryAnalyzer, EXACT_FIRST | PRESERVE_SEP, 256, -1, DEFAULT_MAX_EDITS, DEFAULT_TRANSPOSITIONS,
|
this(indexAnalyzer, null, queryAnalyzer, EXACT_FIRST | PRESERVE_SEP, 256, -1, DEFAULT_MAX_EDITS, DEFAULT_TRANSPOSITIONS,
|
||||||
DEFAULT_NON_FUZZY_PREFIX, DEFAULT_MIN_FUZZY_LENGTH, DEFAULT_UNICODE_AWARE, null, false, 0, SEP_LABEL, PAYLOAD_SEP, END_BYTE, HOLE_CHARACTER);
|
DEFAULT_NON_FUZZY_PREFIX, DEFAULT_MIN_FUZZY_LENGTH, DEFAULT_UNICODE_AWARE, null, false, 0, SEP_LABEL, PAYLOAD_SEP, END_BYTE, HOLE_CHARACTER);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -151,11 +151,11 @@ public final class XFuzzySuggester extends XAnalyzingSuggester {
|
||||||
* @param payloadSep payload separator byte
|
* @param payloadSep payload separator byte
|
||||||
* @param endByte end byte marker byte
|
* @param endByte end byte marker byte
|
||||||
*/
|
*/
|
||||||
public XFuzzySuggester(Analyzer indexAnalyzer, Analyzer queryAnalyzer, int options, int maxSurfaceFormsPerAnalyzedForm, int maxGraphExpansions,
|
public XFuzzySuggester(Analyzer indexAnalyzer, Automaton queryPrefix, Analyzer queryAnalyzer, int options, int maxSurfaceFormsPerAnalyzedForm, int maxGraphExpansions,
|
||||||
int maxEdits, boolean transpositions, int nonFuzzyPrefix, int minFuzzyLength, boolean unicodeAware,
|
int maxEdits, boolean transpositions, int nonFuzzyPrefix, int minFuzzyLength, boolean unicodeAware,
|
||||||
FST<PairOutputs.Pair<Long, BytesRef>> fst, boolean hasPayloads, int maxAnalyzedPathsForOneInput,
|
FST<PairOutputs.Pair<Long, BytesRef>> fst, boolean hasPayloads, int maxAnalyzedPathsForOneInput,
|
||||||
int sepLabel, int payloadSep, int endByte, int holeCharacter) {
|
int sepLabel, int payloadSep, int endByte, int holeCharacter) {
|
||||||
super(indexAnalyzer, queryAnalyzer, options, maxSurfaceFormsPerAnalyzedForm, maxGraphExpansions, true, fst, hasPayloads, maxAnalyzedPathsForOneInput, sepLabel, payloadSep, endByte, holeCharacter);
|
super(indexAnalyzer, queryPrefix, queryAnalyzer, options, maxSurfaceFormsPerAnalyzedForm, maxGraphExpansions, true, fst, hasPayloads, maxAnalyzedPathsForOneInput, sepLabel, payloadSep, endByte, holeCharacter);
|
||||||
if (maxEdits < 0 || maxEdits > LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE) {
|
if (maxEdits < 0 || maxEdits > LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE) {
|
||||||
throw new IllegalArgumentException("maxEdits must be between 0 and " + LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE);
|
throw new IllegalArgumentException("maxEdits must be between 0 and " + LevenshteinAutomata.MAXIMUM_SUPPORTED_DISTANCE);
|
||||||
}
|
}
|
||||||
|
@ -202,11 +202,12 @@ public final class XFuzzySuggester extends XAnalyzingSuggester {
|
||||||
@Override
|
@Override
|
||||||
protected Automaton convertAutomaton(Automaton a) {
|
protected Automaton convertAutomaton(Automaton a) {
|
||||||
if (unicodeAware) {
|
if (unicodeAware) {
|
||||||
Automaton utf8automaton = new UTF32ToUTF8().convert(a);
|
// FLORIAN EDIT: get converted Automaton from superclass
|
||||||
|
Automaton utf8automaton = new UTF32ToUTF8().convert(super.convertAutomaton(a));
|
||||||
BasicOperations.determinize(utf8automaton);
|
BasicOperations.determinize(utf8automaton);
|
||||||
return utf8automaton;
|
return utf8automaton;
|
||||||
} else {
|
} else {
|
||||||
return a;
|
return super.convertAutomaton(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,8 @@ package org.elasticsearch.common.geo;
|
||||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,11 +114,28 @@ public class GeoHashUtils {
|
||||||
/**
|
/**
|
||||||
* Calculate all neighbors of a given geohash cell.
|
* Calculate all neighbors of a given geohash cell.
|
||||||
*
|
*
|
||||||
* @param geohash Geohash of the defines cell
|
* @param geohash Geohash of the defined cell
|
||||||
* @return geohashes of all neighbor cells
|
* @return geohashes of all neighbor cells
|
||||||
*/
|
*/
|
||||||
public static List<String> neighbors(String geohash) {
|
public static Collection<? extends CharSequence> neighbors(String geohash) {
|
||||||
return addNeighbors(geohash, geohash.length(), new ArrayList<String>(8));
|
return addNeighbors(geohash, geohash.length(), new ArrayList<CharSequence>(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an {@link Iterable} which allows to iterate over the cells that
|
||||||
|
* contain a given geohash
|
||||||
|
*
|
||||||
|
* @param geohash Geohash of a cell
|
||||||
|
*
|
||||||
|
* @return {@link Iterable} of path
|
||||||
|
*/
|
||||||
|
public static Iterable<String> path(final String geohash) {
|
||||||
|
return new Iterable<String>() {
|
||||||
|
@Override
|
||||||
|
public Iterator<String> iterator() {
|
||||||
|
return new GeohashPathIterator(geohash);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,7 +182,7 @@ public class GeoHashUtils {
|
||||||
final int yLimit = ((level % 2) == 0) ? 3 : 7;
|
final int yLimit = ((level % 2) == 0) ? 3 : 7;
|
||||||
|
|
||||||
// if the defined neighbor has the same parent a the current cell
|
// if the defined neighbor has the same parent a the current cell
|
||||||
// encode the cell direcly. Otherwise find the cell next to this
|
// encode the cell directly. Otherwise find the cell next to this
|
||||||
// cell recursively. Since encoding wraps around within a cell
|
// cell recursively. Since encoding wraps around within a cell
|
||||||
// it can be encoded here.
|
// it can be encoded here.
|
||||||
if (nx >= 0 && nx <= xLimit && ny >= 0 && ny < yLimit) {
|
if (nx >= 0 && nx <= xLimit && ny >= 0 && ny < yLimit) {
|
||||||
|
@ -180,6 +198,17 @@ public class GeoHashUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all geohashes of the cells next to a given geohash to a list.
|
||||||
|
*
|
||||||
|
* @param geohash Geohash of a specified cell
|
||||||
|
* @param neighbors list to add the neighbors to
|
||||||
|
* @return the given list
|
||||||
|
*/
|
||||||
|
public static final <E extends Collection<? super String>> E addNeighbors(String geohash, E neighbors) {
|
||||||
|
return addNeighbors(geohash, geohash.length(), neighbors);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add all geohashes of the cells next to a given geohash to a list.
|
* Add all geohashes of the cells next to a given geohash to a list.
|
||||||
*
|
*
|
||||||
|
@ -188,10 +217,9 @@ public class GeoHashUtils {
|
||||||
* @param neighbors list to add the neighbors to
|
* @param neighbors list to add the neighbors to
|
||||||
* @return the given list
|
* @return the given list
|
||||||
*/
|
*/
|
||||||
private static final List<String> addNeighbors(String geohash, int length, List<String> neighbors) {
|
public static final <E extends Collection<? super String>> E addNeighbors(String geohash, int length, E neighbors) {
|
||||||
String south = neighbor(geohash, length, 0, -1);
|
String south = neighbor(geohash, length, 0, -1);
|
||||||
String north = neighbor(geohash, length, 0, +1);
|
String north = neighbor(geohash, length, 0, +1);
|
||||||
|
|
||||||
if (north != null) {
|
if (north != null) {
|
||||||
neighbors.add(neighbor(north, length, -1, 0));
|
neighbors.add(neighbor(north, length, -1, 0));
|
||||||
neighbors.add(north);
|
neighbors.add(north);
|
||||||
|
|
|
@ -29,7 +29,7 @@ import java.io.IOException;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class GeoPoint {
|
public final class GeoPoint {
|
||||||
|
|
||||||
public static final String LATITUDE = GeoPointFieldMapper.Names.LAT;
|
public static final String LATITUDE = GeoPointFieldMapper.Names.LAT;
|
||||||
public static final String LONGITUDE = GeoPointFieldMapper.Names.LON;
|
public static final String LONGITUDE = GeoPointFieldMapper.Names.LON;
|
||||||
|
@ -41,6 +41,16 @@ public class GeoPoint {
|
||||||
public GeoPoint() {
|
public GeoPoint() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Geopointform a string. This String must either be a geohash
|
||||||
|
* or a lat-lon tuple.
|
||||||
|
*
|
||||||
|
* @param value String to create the point from
|
||||||
|
*/
|
||||||
|
public GeoPoint(String value) {
|
||||||
|
this.resetFromString(value);
|
||||||
|
}
|
||||||
|
|
||||||
public GeoPoint(double lat, double lon) {
|
public GeoPoint(double lat, double lon) {
|
||||||
this.lat = lat;
|
this.lat = lat;
|
||||||
this.lon = lon;
|
this.lon = lon;
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.common.geo;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class iterates over the cells of a given geohash. Assume geohashes
|
||||||
|
* form a tree, this iterator traverses this tree form a leaf (actual gehash)
|
||||||
|
* to the root (geohash of length 1).
|
||||||
|
*/
|
||||||
|
public final class GeohashPathIterator implements Iterator<String> {
|
||||||
|
|
||||||
|
private final String geohash;
|
||||||
|
private int currentLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link GeohashPathIterator} for a given geohash
|
||||||
|
* @param geohash The geohash to traverse
|
||||||
|
*/
|
||||||
|
public GeohashPathIterator(String geohash) {
|
||||||
|
this.geohash = geohash;
|
||||||
|
this.currentLength = geohash.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return currentLength > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String next() {
|
||||||
|
String result = geohash.substring(0, currentLength);
|
||||||
|
currentLength--;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException("unable to remove a geohash from this path");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1036,6 +1036,14 @@ public final class XContentBuilder implements BytesStream {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public XContentBuilder latlon(String name, double lat, double lon) throws IOException {
|
||||||
|
return startObject(name).field("lat", lat).field("lon", lon).endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public XContentBuilder latlon(double lat, double lon) throws IOException {
|
||||||
|
return startObject().field("lat", lat).field("lon", lon).endObject();
|
||||||
|
}
|
||||||
|
|
||||||
public XContentBuilder copyCurrentStructure(XContentParser parser) throws IOException {
|
public XContentBuilder copyCurrentStructure(XContentParser parser) throws IOException {
|
||||||
generator.copyCurrentStructure(parser);
|
generator.copyCurrentStructure(parser);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package org.elasticsearch.index.mapper.core;
|
package org.elasticsearch.index.mapper.core;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import org.apache.lucene.analysis.Analyzer;
|
import org.apache.lucene.analysis.Analyzer;
|
||||||
import org.apache.lucene.analysis.TokenStream;
|
import org.apache.lucene.analysis.TokenStream;
|
||||||
|
@ -27,11 +28,11 @@ import org.apache.lucene.document.FieldType;
|
||||||
import org.apache.lucene.search.suggest.analyzing.XAnalyzingSuggester;
|
import org.apache.lucene.search.suggest.analyzing.XAnalyzingSuggester;
|
||||||
import org.apache.lucene.util.BytesRef;
|
import org.apache.lucene.util.BytesRef;
|
||||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
import org.elasticsearch.common.ParseField;
|
import org.elasticsearch.common.ParseField;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
import org.elasticsearch.common.xcontent.*;
|
||||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
|
||||||
import org.elasticsearch.common.xcontent.XContentParser;
|
|
||||||
import org.elasticsearch.common.xcontent.XContentParser.NumberType;
|
import org.elasticsearch.common.xcontent.XContentParser.NumberType;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||||
import org.elasticsearch.index.analysis.NamedAnalyzer;
|
import org.elasticsearch.index.analysis.NamedAnalyzer;
|
||||||
import org.elasticsearch.index.codec.postingsformat.PostingsFormatProvider;
|
import org.elasticsearch.index.codec.postingsformat.PostingsFormatProvider;
|
||||||
import org.elasticsearch.index.fielddata.FieldDataType;
|
import org.elasticsearch.index.fielddata.FieldDataType;
|
||||||
|
@ -40,13 +41,13 @@ import org.elasticsearch.index.similarity.SimilarityProvider;
|
||||||
import org.elasticsearch.search.suggest.completion.AnalyzingCompletionLookupProvider;
|
import org.elasticsearch.search.suggest.completion.AnalyzingCompletionLookupProvider;
|
||||||
import org.elasticsearch.search.suggest.completion.CompletionPostingsFormatProvider;
|
import org.elasticsearch.search.suggest.completion.CompletionPostingsFormatProvider;
|
||||||
import org.elasticsearch.search.suggest.completion.CompletionTokenStream;
|
import org.elasticsearch.search.suggest.completion.CompletionTokenStream;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextBuilder;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping.ContextConfig;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.elasticsearch.index.mapper.MapperBuilders.completionField;
|
import static org.elasticsearch.index.mapper.MapperBuilders.completionField;
|
||||||
import static org.elasticsearch.index.mapper.core.TypeParsers.parseMultiField;
|
import static org.elasticsearch.index.mapper.core.TypeParsers.parseMultiField;
|
||||||
|
@ -87,10 +88,11 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
public static final String CONTENT_FIELD_NAME_OUTPUT = "output";
|
public static final String CONTENT_FIELD_NAME_OUTPUT = "output";
|
||||||
public static final String CONTENT_FIELD_NAME_PAYLOAD = "payload";
|
public static final String CONTENT_FIELD_NAME_PAYLOAD = "payload";
|
||||||
public static final String CONTENT_FIELD_NAME_WEIGHT = "weight";
|
public static final String CONTENT_FIELD_NAME_WEIGHT = "weight";
|
||||||
|
public static final String CONTEXT = "context";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<String> ALLOWED_CONTENT_FIELD_NAMES = Sets.newHashSet(Fields.CONTENT_FIELD_NAME_INPUT,
|
public static Set<String> ALLOWED_CONTENT_FIELD_NAMES = Sets.newHashSet(Fields.CONTENT_FIELD_NAME_INPUT,
|
||||||
Fields.CONTENT_FIELD_NAME_OUTPUT, Fields.CONTENT_FIELD_NAME_PAYLOAD, Fields.CONTENT_FIELD_NAME_WEIGHT);
|
Fields.CONTENT_FIELD_NAME_OUTPUT, Fields.CONTENT_FIELD_NAME_PAYLOAD, Fields.CONTENT_FIELD_NAME_WEIGHT, Fields.CONTEXT);
|
||||||
|
|
||||||
public static class Builder extends AbstractFieldMapper.Builder<Builder, CompletionFieldMapper> {
|
public static class Builder extends AbstractFieldMapper.Builder<Builder, CompletionFieldMapper> {
|
||||||
|
|
||||||
|
@ -98,6 +100,7 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
private boolean payloads = Defaults.DEFAULT_HAS_PAYLOADS;
|
private boolean payloads = Defaults.DEFAULT_HAS_PAYLOADS;
|
||||||
private boolean preservePositionIncrements = Defaults.DEFAULT_POSITION_INCREMENTS;
|
private boolean preservePositionIncrements = Defaults.DEFAULT_POSITION_INCREMENTS;
|
||||||
private int maxInputLength = Defaults.DEFAULT_MAX_INPUT_LENGTH;
|
private int maxInputLength = Defaults.DEFAULT_MAX_INPUT_LENGTH;
|
||||||
|
private SortedMap<String, ContextMapping> contextMapping = ContextMapping.EMPTY_MAPPING;
|
||||||
|
|
||||||
public Builder(String name) {
|
public Builder(String name) {
|
||||||
super(name, new FieldType(Defaults.FIELD_TYPE));
|
super(name, new FieldType(Defaults.FIELD_TYPE));
|
||||||
|
@ -127,11 +130,17 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder contextMapping(SortedMap<String, ContextMapping> contextMapping) {
|
||||||
|
this.contextMapping = contextMapping;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletionFieldMapper build(Mapper.BuilderContext context) {
|
public CompletionFieldMapper build(Mapper.BuilderContext context) {
|
||||||
return new CompletionFieldMapper(buildNames(context), indexAnalyzer, searchAnalyzer, postingsProvider, similarity, payloads,
|
return new CompletionFieldMapper(buildNames(context), indexAnalyzer, searchAnalyzer, postingsProvider, similarity, payloads,
|
||||||
preserveSeparators, preservePositionIncrements, maxInputLength, multiFieldsBuilder.build(this, context), copyTo);
|
preserveSeparators, preservePositionIncrements, maxInputLength, multiFieldsBuilder.build(this, context), copyTo, this.contextMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class TypeParser implements Mapper.TypeParser {
|
public static class TypeParser implements Mapper.TypeParser {
|
||||||
|
@ -163,6 +172,8 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
builder.maxInputLength(Integer.parseInt(fieldNode.toString()));
|
builder.maxInputLength(Integer.parseInt(fieldNode.toString()));
|
||||||
} else if ("fields".equals(fieldName) || "path".equals(fieldName)) {
|
} else if ("fields".equals(fieldName) || "path".equals(fieldName)) {
|
||||||
parseMultiField(builder, name, node, parserContext, fieldName, fieldNode);
|
parseMultiField(builder, name, node, parserContext, fieldName, fieldNode);
|
||||||
|
} else if (fieldName.equals(Fields.CONTEXT)) {
|
||||||
|
builder.contextMapping(ContextBuilder.loadMappings(fieldNode));
|
||||||
} else {
|
} else {
|
||||||
throw new MapperParsingException("Unknown field [" + fieldName + "]");
|
throw new MapperParsingException("Unknown field [" + fieldName + "]");
|
||||||
}
|
}
|
||||||
|
@ -197,9 +208,14 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
private final boolean preservePositionIncrements;
|
private final boolean preservePositionIncrements;
|
||||||
private final boolean preserveSeparators;
|
private final boolean preserveSeparators;
|
||||||
private int maxInputLength;
|
private int maxInputLength;
|
||||||
|
private final SortedMap<String, ContextMapping> contextMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param contextMappings Configuration of context type. If none should be used set {@link ContextMapping.EMPTY_MAPPING}
|
||||||
|
*/
|
||||||
public CompletionFieldMapper(Names names, NamedAnalyzer indexAnalyzer, NamedAnalyzer searchAnalyzer, PostingsFormatProvider postingsProvider, SimilarityProvider similarity, boolean payloads,
|
public CompletionFieldMapper(Names names, NamedAnalyzer indexAnalyzer, NamedAnalyzer searchAnalyzer, PostingsFormatProvider postingsProvider, SimilarityProvider similarity, boolean payloads,
|
||||||
boolean preserveSeparators, boolean preservePositionIncrements, int maxInputLength, MultiFields multiFields, CopyTo copyTo) {
|
boolean preserveSeparators, boolean preservePositionIncrements, int maxInputLength, MultiFields multiFields, CopyTo copyTo, SortedMap<String, ContextMapping> contextMappings) {
|
||||||
super(names, 1.0f, Defaults.FIELD_TYPE, null, indexAnalyzer, searchAnalyzer, postingsProvider, null, similarity, null, null, null, multiFields, copyTo);
|
super(names, 1.0f, Defaults.FIELD_TYPE, null, indexAnalyzer, searchAnalyzer, postingsProvider, null, similarity, null, null, null, multiFields, copyTo);
|
||||||
analyzingSuggestLookupProvider = new AnalyzingCompletionLookupProvider(preserveSeparators, false, preservePositionIncrements, payloads);
|
analyzingSuggestLookupProvider = new AnalyzingCompletionLookupProvider(preserveSeparators, false, preservePositionIncrements, payloads);
|
||||||
this.completionPostingsFormatProvider = new CompletionPostingsFormatProvider("completion", postingsProvider, analyzingSuggestLookupProvider);
|
this.completionPostingsFormatProvider = new CompletionPostingsFormatProvider("completion", postingsProvider, analyzingSuggestLookupProvider);
|
||||||
|
@ -207,9 +223,9 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
this.payloads = payloads;
|
this.payloads = payloads;
|
||||||
this.preservePositionIncrements = preservePositionIncrements;
|
this.preservePositionIncrements = preservePositionIncrements;
|
||||||
this.maxInputLength = maxInputLength;
|
this.maxInputLength = maxInputLength;
|
||||||
|
this.contextMapping = contextMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PostingsFormatProvider postingsFormatProvider() {
|
public PostingsFormatProvider postingsFormatProvider() {
|
||||||
return this.completionPostingsFormatProvider;
|
return this.completionPostingsFormatProvider;
|
||||||
|
@ -225,6 +241,8 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
long weight = -1;
|
long weight = -1;
|
||||||
List<String> inputs = Lists.newArrayListWithExpectedSize(4);
|
List<String> inputs = Lists.newArrayListWithExpectedSize(4);
|
||||||
|
|
||||||
|
SortedMap<String, ContextConfig> contextConfig = null;
|
||||||
|
|
||||||
if (token == XContentParser.Token.VALUE_STRING) {
|
if (token == XContentParser.Token.VALUE_STRING) {
|
||||||
inputs.add(parser.text());
|
inputs.add(parser.text());
|
||||||
multiFields.parse(this, context);
|
multiFields.parse(this, context);
|
||||||
|
@ -236,6 +254,28 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
if (!ALLOWED_CONTENT_FIELD_NAMES.contains(currentFieldName)) {
|
if (!ALLOWED_CONTENT_FIELD_NAMES.contains(currentFieldName)) {
|
||||||
throw new ElasticsearchIllegalArgumentException("Unknown field name[" + currentFieldName + "], must be one of " + ALLOWED_CONTENT_FIELD_NAMES);
|
throw new ElasticsearchIllegalArgumentException("Unknown field name[" + currentFieldName + "], must be one of " + ALLOWED_CONTENT_FIELD_NAMES);
|
||||||
}
|
}
|
||||||
|
} else if (Fields.CONTEXT.equals(currentFieldName)) {
|
||||||
|
SortedMap<String, ContextConfig> configs = Maps.newTreeMap();
|
||||||
|
|
||||||
|
if (token == Token.START_OBJECT) {
|
||||||
|
while ((token = parser.nextToken()) != Token.END_OBJECT) {
|
||||||
|
String name = parser.text();
|
||||||
|
ContextMapping mapping = contextMapping.get(name);
|
||||||
|
if (mapping == null) {
|
||||||
|
throw new ElasticsearchParseException("context [" + name + "] is not defined");
|
||||||
|
} else {
|
||||||
|
token = parser.nextToken();
|
||||||
|
configs.put(name, mapping.parseContext(context, parser));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contextConfig = Maps.newTreeMap();
|
||||||
|
for (ContextMapping mapping : contextMapping.values()) {
|
||||||
|
ContextConfig config = configs.get(mapping.name());
|
||||||
|
contextConfig.put(mapping.name(), config==null ? mapping.defaultConfig() : config);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("context must be an object");
|
||||||
|
}
|
||||||
} else if (Fields.CONTENT_FIELD_NAME_PAYLOAD.equals(currentFieldName)) {
|
} else if (Fields.CONTENT_FIELD_NAME_PAYLOAD.equals(currentFieldName)) {
|
||||||
if (!isStoringPayloads()) {
|
if (!isStoringPayloads()) {
|
||||||
throw new MapperException("Payloads disabled in mapping");
|
throw new MapperException("Payloads disabled in mapping");
|
||||||
|
@ -276,23 +316,49 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(contextConfig == null) {
|
||||||
|
contextConfig = Maps.newTreeMap();
|
||||||
|
for (ContextMapping mapping : contextMapping.values()) {
|
||||||
|
contextConfig.put(mapping.name(), mapping.defaultConfig());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final ContextMapping.Context ctx = new ContextMapping.Context(contextConfig, context.doc());
|
||||||
|
|
||||||
payload = payload == null ? EMPTY : payload;
|
payload = payload == null ? EMPTY : payload;
|
||||||
if (surfaceForm == null) { // no surface form use the input
|
if (surfaceForm == null) { // no surface form use the input
|
||||||
for (String input : inputs) {
|
for (String input : inputs) {
|
||||||
BytesRef suggestPayload = analyzingSuggestLookupProvider.buildPayload(new BytesRef(
|
BytesRef suggestPayload = analyzingSuggestLookupProvider.buildPayload(new BytesRef(
|
||||||
input), weight, payload);
|
input), weight, payload);
|
||||||
context.doc().add(getCompletionField(input, suggestPayload));
|
context.doc().add(getCompletionField(ctx, input, suggestPayload));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BytesRef suggestPayload = analyzingSuggestLookupProvider.buildPayload(new BytesRef(
|
BytesRef suggestPayload = analyzingSuggestLookupProvider.buildPayload(new BytesRef(
|
||||||
surfaceForm), weight, payload);
|
surfaceForm), weight, payload);
|
||||||
for (String input : inputs) {
|
for (String input : inputs) {
|
||||||
context.doc().add(getCompletionField(input, suggestPayload));
|
context.doc().add(getCompletionField(ctx, input, suggestPayload));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the context mapping associated with this completion field.
|
||||||
|
*/
|
||||||
|
public SortedMap<String, ContextMapping> getContextMapping() {
|
||||||
|
return contextMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if a context mapping has been defined */
|
||||||
|
public boolean requiresContext() {
|
||||||
|
return !contextMapping.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
public Field getCompletionField(String input, BytesRef payload) {
|
public Field getCompletionField(String input, BytesRef payload) {
|
||||||
|
return getCompletionField(ContextMapping.EMPTY_CONTEXT, input, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field getCompletionField(ContextMapping.Context ctx, String input, BytesRef payload) {
|
||||||
final String originalInput = input;
|
final String originalInput = input;
|
||||||
if (input.length() > maxInputLength) {
|
if (input.length() > maxInputLength) {
|
||||||
final int len = correctSubStringLen(input, Math.min(maxInputLength, input.length()));
|
final int len = correctSubStringLen(input, Math.min(maxInputLength, input.length()));
|
||||||
|
@ -305,7 +371,7 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
+ "] at position " + i + " is a reserved character");
|
+ "] at position " + i + " is a reserved character");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new SuggestField(names.indexName(), input, this.fieldType, payload, analyzingSuggestLookupProvider);
|
return new SuggestField(names.indexName(), ctx, input, this.fieldType, payload, analyzingSuggestLookupProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int correctSubStringLen(String input, int len) {
|
public static int correctSubStringLen(String input, int len) {
|
||||||
|
@ -324,27 +390,29 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
private static final class SuggestField extends Field {
|
private static final class SuggestField extends Field {
|
||||||
private final BytesRef payload;
|
private final BytesRef payload;
|
||||||
private final CompletionTokenStream.ToFiniteStrings toFiniteStrings;
|
private final CompletionTokenStream.ToFiniteStrings toFiniteStrings;
|
||||||
|
private final ContextMapping.Context ctx;
|
||||||
|
|
||||||
public SuggestField(String name, Reader value, FieldType type, BytesRef payload, CompletionTokenStream.ToFiniteStrings toFiniteStrings) {
|
public SuggestField(String name, ContextMapping.Context ctx, Reader value, FieldType type, BytesRef payload, CompletionTokenStream.ToFiniteStrings toFiniteStrings) {
|
||||||
super(name, value, type);
|
super(name, value, type);
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
this.toFiniteStrings = toFiniteStrings;
|
this.toFiniteStrings = toFiniteStrings;
|
||||||
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SuggestField(String name, String value, FieldType type, BytesRef payload, CompletionTokenStream.ToFiniteStrings toFiniteStrings) {
|
public SuggestField(String name, ContextMapping.Context ctx, String value, FieldType type, BytesRef payload, CompletionTokenStream.ToFiniteStrings toFiniteStrings) {
|
||||||
super(name, value, type);
|
super(name, value, type);
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
this.toFiniteStrings = toFiniteStrings;
|
this.toFiniteStrings = toFiniteStrings;
|
||||||
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TokenStream tokenStream(Analyzer analyzer) throws IOException {
|
public TokenStream tokenStream(Analyzer analyzer) throws IOException {
|
||||||
TokenStream ts = super.tokenStream(analyzer);
|
TokenStream ts = ctx.wrapTokenStream(super.tokenStream(analyzer));
|
||||||
return new CompletionTokenStream(ts, payload, toFiniteStrings);
|
return new CompletionTokenStream(ts, payload, toFiniteStrings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
builder.startObject(name())
|
builder.startObject(name())
|
||||||
|
@ -360,6 +428,15 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
builder.field(Fields.PRESERVE_POSITION_INCREMENTS.getPreferredName(), this.preservePositionIncrements);
|
builder.field(Fields.PRESERVE_POSITION_INCREMENTS.getPreferredName(), this.preservePositionIncrements);
|
||||||
builder.field(Fields.MAX_INPUT_LENGTH.getPreferredName(), this.maxInputLength);
|
builder.field(Fields.MAX_INPUT_LENGTH.getPreferredName(), this.maxInputLength);
|
||||||
multiFields.toXContent(builder, params);
|
multiFields.toXContent(builder, params);
|
||||||
|
|
||||||
|
if(!contextMapping.isEmpty()) {
|
||||||
|
builder.startObject(Fields.CONTEXT);
|
||||||
|
for (ContextMapping mapping : contextMapping.values()) {
|
||||||
|
builder.value(mapping);
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
return builder.endObject();
|
return builder.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +444,6 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
protected void parseCreateField(ParseContext context, List<Field> fields) throws IOException {
|
protected void parseCreateField(ParseContext context, List<Field> fields) throws IOException {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String contentType() {
|
protected String contentType() {
|
||||||
return CONTENT_TYPE;
|
return CONTENT_TYPE;
|
||||||
|
@ -418,6 +494,9 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
if (preserveSeparators != fieldMergeWith.preserveSeparators) {
|
if (preserveSeparators != fieldMergeWith.preserveSeparators) {
|
||||||
mergeContext.addConflict("mapper [" + names.fullName() + "] has different 'preserve_separators' values");
|
mergeContext.addConflict("mapper [" + names.fullName() + "] has different 'preserve_separators' values");
|
||||||
}
|
}
|
||||||
|
if(!ContextMapping.mappingsAreEqual(getContextMapping(), fieldMergeWith.getContextMapping())) {
|
||||||
|
mergeContext.addConflict("mapper [" + names.fullName() + "] has different 'context_mapping' values");
|
||||||
|
}
|
||||||
if (!mergeContext.mergeFlags().simulate()) {
|
if (!mergeContext.mergeFlags().simulate()) {
|
||||||
this.maxInputLength = fieldMergeWith.maxInputLength;
|
this.maxInputLength = fieldMergeWith.maxInputLength;
|
||||||
}
|
}
|
||||||
|
@ -434,6 +513,7 @@ public class CompletionFieldMapper extends AbstractFieldMapper<String> {
|
||||||
case XAnalyzingSuggester.END_BYTE:
|
case XAnalyzingSuggester.END_BYTE:
|
||||||
case XAnalyzingSuggester.SEP_LABEL:
|
case XAnalyzingSuggester.SEP_LABEL:
|
||||||
case XAnalyzingSuggester.HOLE_CHARACTER:
|
case XAnalyzingSuggester.HOLE_CHARACTER:
|
||||||
|
case ContextMapping.SEPARATOR:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.elasticsearch.index.mapper.core.StringFieldMapper;
|
||||||
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
|
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,7 +72,7 @@ public class GeohashCellFilter {
|
||||||
* @param geohashes optional array of additional geohashes
|
* @param geohashes optional array of additional geohashes
|
||||||
* @return a new GeoBoundinboxfilter
|
* @return a new GeoBoundinboxfilter
|
||||||
*/
|
*/
|
||||||
public static Filter create(QueryParseContext context, GeoPointFieldMapper fieldMapper, String geohash, @Nullable List<String> geohashes) {
|
public static Filter create(QueryParseContext context, GeoPointFieldMapper fieldMapper, String geohash, @Nullable List<CharSequence> geohashes) {
|
||||||
if (fieldMapper.geoHashStringMapper() == null) {
|
if (fieldMapper.geoHashStringMapper() == null) {
|
||||||
throw new ElasticsearchIllegalArgumentException("geohash filter needs geohash_prefix to be enabled");
|
throw new ElasticsearchIllegalArgumentException("geohash filter needs geohash_prefix to be enabled");
|
||||||
}
|
}
|
||||||
|
@ -254,7 +255,7 @@ public class GeohashCellFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (neighbors) {
|
if (neighbors) {
|
||||||
return create(parseContext, geoMapper, geohash, GeoHashUtils.neighbors(geohash));
|
return create(parseContext, geoMapper, geohash, GeoHashUtils.addNeighbors(geohash, new ArrayList<CharSequence>(8)));
|
||||||
} else {
|
} else {
|
||||||
return create(parseContext, geoMapper, geohash, null);
|
return create(parseContext, geoMapper, geohash, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,9 @@ import java.util.List;
|
||||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||||
import org.elasticsearch.common.xcontent.ToXContent;
|
import org.elasticsearch.common.xcontent.ToXContent;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping.ContextQuery;
|
||||||
|
import org.elasticsearch.search.suggest.context.CategoryContextMapping;
|
||||||
|
import org.elasticsearch.search.suggest.context.GeolocationContextMapping;
|
||||||
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
|
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
|
||||||
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
|
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
|
||||||
|
|
||||||
|
@ -124,11 +127,74 @@ public class SuggestBuilder implements ToXContent {
|
||||||
private Integer size;
|
private Integer size;
|
||||||
private Integer shardSize;
|
private Integer shardSize;
|
||||||
|
|
||||||
|
private List<ContextQuery> contextQueries = new ArrayList<ContextQuery>();
|
||||||
|
|
||||||
public SuggestionBuilder(String name, String suggester) {
|
public SuggestionBuilder(String name, String suggester) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.suggester = suggester;
|
this.suggester = suggester;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private T addContextQuery(ContextQuery ctx) {
|
||||||
|
this.contextQueries.add(ctx);
|
||||||
|
return (T) this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a Geolocation for suggestions. See {@link GeoContextMapping}.
|
||||||
|
* @param lat Latitude of the location
|
||||||
|
* @param lon Longitude of the Location
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public T addGeoLocation(String name, double lat, double lon) {
|
||||||
|
return addContextQuery(GeolocationContextMapping.query(name, lat, lon));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a Geolocation for suggestions. See {@link GeoContextMapping}.
|
||||||
|
* @param geohash Geohash of the location
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public T addGeoLocation(String name, String geohash) {
|
||||||
|
return addContextQuery(GeolocationContextMapping.query(name, geohash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a Category for suggestions. See {@link CategoryMapping}.
|
||||||
|
* @param category name of the category
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public T addCategory(String name, CharSequence...categories) {
|
||||||
|
return addContextQuery(CategoryContextMapping.query(name, categories));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a Category for suggestions. See {@link CategoryMapping}.
|
||||||
|
* @param category name of the category
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public T addCategory(String name, Iterable<? extends CharSequence> categories) {
|
||||||
|
return addContextQuery(CategoryContextMapping.query(name, categories));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a Context Field for suggestions. See {@link CategoryContextMapping}.
|
||||||
|
* @param category name of the category
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public T addContextField(String name, CharSequence...fieldvalues) {
|
||||||
|
return addContextQuery(CategoryContextMapping.query(name, fieldvalues));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup a Context Field for suggestions. See {@link CategoryContextMapping}.
|
||||||
|
* @param category name of the category
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public T addContextField(String name, Iterable<? extends CharSequence> fieldvalues) {
|
||||||
|
return addContextQuery(CategoryContextMapping.query(name, fieldvalues));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as in {@link SuggestBuilder#setText(String)}, but in the suggestion scope.
|
* Same as in {@link SuggestBuilder#setText(String)}, but in the suggestion scope.
|
||||||
*/
|
*/
|
||||||
|
@ -157,6 +223,14 @@ public class SuggestBuilder implements ToXContent {
|
||||||
if (shardSize != null) {
|
if (shardSize != null) {
|
||||||
builder.field("shard_size", shardSize);
|
builder.field("shard_size", shardSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!contextQueries.isEmpty()) {
|
||||||
|
builder.startObject("context");
|
||||||
|
for (ContextQuery query : contextQueries) {
|
||||||
|
query.toXContent(builder, params);
|
||||||
|
}
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
builder = innerToXContent(builder, params);
|
builder = innerToXContent(builder, params);
|
||||||
builder.endObject();
|
builder.endObject();
|
||||||
builder.endObject();
|
builder.endObject();
|
||||||
|
|
|
@ -18,13 +18,12 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.search.suggest;
|
package org.elasticsearch.search.suggest;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.lucene.analysis.Analyzer;
|
import org.apache.lucene.analysis.Analyzer;
|
||||||
import org.apache.lucene.util.BytesRef;
|
import org.apache.lucene.util.BytesRef;
|
||||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
public class SuggestionSearchContext {
|
public class SuggestionSearchContext {
|
||||||
|
@ -121,6 +120,4 @@ public class SuggestionSearchContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,15 +31,14 @@ import org.apache.lucene.store.IndexOutput;
|
||||||
import org.apache.lucene.util.BytesRef;
|
import org.apache.lucene.util.BytesRef;
|
||||||
import org.apache.lucene.util.IOUtils;
|
import org.apache.lucene.util.IOUtils;
|
||||||
import org.apache.lucene.util.IntsRef;
|
import org.apache.lucene.util.IntsRef;
|
||||||
import org.apache.lucene.util.fst.ByteSequenceOutputs;
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
import org.apache.lucene.util.fst.FST;
|
import org.apache.lucene.util.fst.*;
|
||||||
import org.apache.lucene.util.fst.PairOutputs;
|
|
||||||
import org.apache.lucene.util.fst.PairOutputs.Pair;
|
import org.apache.lucene.util.fst.PairOutputs.Pair;
|
||||||
import org.apache.lucene.util.fst.PositiveIntOutputs;
|
|
||||||
import org.elasticsearch.common.regex.Regex;
|
import org.elasticsearch.common.regex.Regex;
|
||||||
import org.elasticsearch.index.mapper.FieldMapper;
|
import org.elasticsearch.index.mapper.core.CompletionFieldMapper;
|
||||||
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.CompletionLookupProvider;
|
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.CompletionLookupProvider;
|
||||||
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.LookupFactory;
|
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.LookupFactory;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping.ContextQuery;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -74,7 +73,7 @@ public class AnalyzingCompletionLookupProvider extends CompletionLookupProvider
|
||||||
int options = preserveSep ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
int options = preserveSep ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
||||||
// needs to fixed in the suggester first before it can be supported
|
// needs to fixed in the suggester first before it can be supported
|
||||||
//options |= exactFirst ? XAnalyzingSuggester.EXACT_FIRST : 0;
|
//options |= exactFirst ? XAnalyzingSuggester.EXACT_FIRST : 0;
|
||||||
prototype = new XAnalyzingSuggester(null, null, options, maxSurfaceFormsPerAnalyzedForm, maxGraphExpansions, preservePositionIncrements, null, false, 1, XAnalyzingSuggester.SEP_LABEL, XAnalyzingSuggester.PAYLOAD_SEP, XAnalyzingSuggester.END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
prototype = new XAnalyzingSuggester(null, null, null, options, maxSurfaceFormsPerAnalyzedForm, maxGraphExpansions, preservePositionIncrements, null, false, 1, XAnalyzingSuggester.SEP_LABEL, XAnalyzingSuggester.PAYLOAD_SEP, XAnalyzingSuggester.END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -199,9 +198,6 @@ public class AnalyzingCompletionLookupProvider extends CompletionLookupProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LookupFactory load(IndexInput input) throws IOException {
|
public LookupFactory load(IndexInput input) throws IOException {
|
||||||
long sizeInBytes = 0;
|
long sizeInBytes = 0;
|
||||||
|
@ -256,25 +252,26 @@ public class AnalyzingCompletionLookupProvider extends CompletionLookupProvider
|
||||||
final long ramBytesUsed = sizeInBytes;
|
final long ramBytesUsed = sizeInBytes;
|
||||||
return new LookupFactory() {
|
return new LookupFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Lookup getLookup(FieldMapper<?> mapper, CompletionSuggestionContext suggestionContext) {
|
public Lookup getLookup(CompletionFieldMapper mapper, CompletionSuggestionContext suggestionContext) {
|
||||||
AnalyzingSuggestHolder analyzingSuggestHolder = lookupMap.get(mapper.names().indexName());
|
AnalyzingSuggestHolder analyzingSuggestHolder = lookupMap.get(mapper.names().indexName());
|
||||||
if (analyzingSuggestHolder == null) {
|
if (analyzingSuggestHolder == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int flags = analyzingSuggestHolder.preserveSep ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
int flags = analyzingSuggestHolder.getPreserveSeparator() ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
||||||
|
|
||||||
|
final XAnalyzingSuggester suggester;
|
||||||
|
final Automaton queryPrefix = mapper.requiresContext() ? ContextQuery.toAutomaton(analyzingSuggestHolder.getPreserveSeparator(), suggestionContext.getContextQueries()) : null;
|
||||||
|
|
||||||
XAnalyzingSuggester suggester;
|
|
||||||
if (suggestionContext.isFuzzy()) {
|
if (suggestionContext.isFuzzy()) {
|
||||||
suggester = new XFuzzySuggester(mapper.indexAnalyzer(), mapper.searchAnalyzer(), flags,
|
suggester = new XFuzzySuggester(mapper.indexAnalyzer(), queryPrefix, mapper.searchAnalyzer(), flags,
|
||||||
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
||||||
suggestionContext.getFuzzyEditDistance(), suggestionContext.isFuzzyTranspositions(),
|
suggestionContext.getFuzzyEditDistance(), suggestionContext.isFuzzyTranspositions(),
|
||||||
suggestionContext.getFuzzyPrefixLength(), suggestionContext.getFuzzyMinLength(), suggestionContext.isFuzzyUnicodeAware(),
|
suggestionContext.getFuzzyPrefixLength(), suggestionContext.getFuzzyMinLength(), suggestionContext.isFuzzyUnicodeAware(),
|
||||||
analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
||||||
analyzingSuggestHolder.maxAnalyzedPathsForOneInput, analyzingSuggestHolder.sepLabel, analyzingSuggestHolder.payloadSep, analyzingSuggestHolder.endByte,
|
analyzingSuggestHolder.maxAnalyzedPathsForOneInput, analyzingSuggestHolder.sepLabel, analyzingSuggestHolder.payloadSep, analyzingSuggestHolder.endByte,
|
||||||
analyzingSuggestHolder.holeCharacter);
|
analyzingSuggestHolder.holeCharacter);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
suggester = new XAnalyzingSuggester(mapper.indexAnalyzer(), mapper.searchAnalyzer(), flags,
|
suggester = new XAnalyzingSuggester(mapper.indexAnalyzer(), queryPrefix, mapper.searchAnalyzer(), flags,
|
||||||
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
||||||
analyzingSuggestHolder.preservePositionIncrements, analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
analyzingSuggestHolder.preservePositionIncrements, analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
||||||
analyzingSuggestHolder.maxAnalyzedPathsForOneInput, analyzingSuggestHolder.sepLabel, analyzingSuggestHolder.payloadSep, analyzingSuggestHolder.endByte,
|
analyzingSuggestHolder.maxAnalyzedPathsForOneInput, analyzingSuggestHolder.sepLabel, analyzingSuggestHolder.payloadSep, analyzingSuggestHolder.endByte,
|
||||||
|
@ -309,7 +306,7 @@ public class AnalyzingCompletionLookupProvider extends CompletionLookupProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
AnalyzingSuggestHolder getAnalyzingSuggestHolder(FieldMapper<?> mapper) {
|
AnalyzingSuggestHolder getAnalyzingSuggestHolder(CompletionFieldMapper mapper) {
|
||||||
return lookupMap.get(mapper.names().indexName());
|
return lookupMap.get(mapper.names().indexName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,6 +348,18 @@ public class AnalyzingCompletionLookupProvider extends CompletionLookupProvider
|
||||||
this.endByte = endByte;
|
this.endByte = endByte;
|
||||||
this.holeCharacter = holeCharacter;
|
this.holeCharacter = holeCharacter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getPreserveSeparator() {
|
||||||
|
return preserveSep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getPreservePositionIncrements() {
|
||||||
|
return preservePositionIncrements;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPayloads() {
|
||||||
|
return hasPayloads;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,11 +28,10 @@ import org.apache.lucene.store.IOContext.Context;
|
||||||
import org.apache.lucene.store.*;
|
import org.apache.lucene.store.*;
|
||||||
import org.apache.lucene.util.BytesRef;
|
import org.apache.lucene.util.BytesRef;
|
||||||
import org.apache.lucene.util.IOUtils;
|
import org.apache.lucene.util.IOUtils;
|
||||||
import org.apache.lucene.util.RamUsageEstimator;
|
|
||||||
import org.elasticsearch.ElasticsearchIllegalStateException;
|
import org.elasticsearch.ElasticsearchIllegalStateException;
|
||||||
import org.elasticsearch.common.logging.ESLogger;
|
import org.elasticsearch.common.logging.ESLogger;
|
||||||
import org.elasticsearch.common.logging.Loggers;
|
import org.elasticsearch.common.logging.Loggers;
|
||||||
import org.elasticsearch.index.mapper.FieldMapper;
|
import org.elasticsearch.index.mapper.core.CompletionFieldMapper;
|
||||||
import org.elasticsearch.search.suggest.completion.CompletionTokenStream.ToFiniteStrings;
|
import org.elasticsearch.search.suggest.completion.CompletionTokenStream.ToFiniteStrings;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
@ -284,7 +283,7 @@ public class Completion090PostingsFormat extends PostingsFormat {
|
||||||
this.lookup = lookup;
|
this.lookup = lookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Lookup getLookup(FieldMapper<?> mapper, CompletionSuggestionContext suggestionContext) {
|
public Lookup getLookup(CompletionFieldMapper mapper, CompletionSuggestionContext suggestionContext) {
|
||||||
return lookup.getLookup(mapper, suggestionContext);
|
return lookup.getLookup(mapper, suggestionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,9 +363,9 @@ public class Completion090PostingsFormat extends PostingsFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static abstract class LookupFactory {
|
public static abstract class LookupFactory {
|
||||||
public abstract Lookup getLookup(FieldMapper<?> mapper, CompletionSuggestionContext suggestionContext);
|
public abstract Lookup getLookup(CompletionFieldMapper mapper, CompletionSuggestionContext suggestionContext);
|
||||||
public abstract CompletionStats stats(String ... fields);
|
public abstract CompletionStats stats(String ... fields);
|
||||||
abstract AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder getAnalyzingSuggestHolder(FieldMapper<?> mapper);
|
abstract AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder getAnalyzingSuggestHolder(CompletionFieldMapper mapper);
|
||||||
public abstract long ramBytesUsed();
|
public abstract long ramBytesUsed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,19 @@ package org.elasticsearch.search.suggest.completion;
|
||||||
|
|
||||||
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
import org.elasticsearch.ElasticsearchIllegalArgumentException;
|
||||||
import org.elasticsearch.common.ParseField;
|
import org.elasticsearch.common.ParseField;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.unit.Fuzziness;
|
import org.elasticsearch.common.unit.Fuzziness;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
import org.elasticsearch.common.xcontent.XContentParser;
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
import org.elasticsearch.index.mapper.MapperService;
|
import org.elasticsearch.index.mapper.MapperService;
|
||||||
|
import org.elasticsearch.index.mapper.core.CompletionFieldMapper;
|
||||||
import org.elasticsearch.search.suggest.SuggestContextParser;
|
import org.elasticsearch.search.suggest.SuggestContextParser;
|
||||||
import org.elasticsearch.search.suggest.SuggestionSearchContext;
|
import org.elasticsearch.search.suggest.SuggestionSearchContext;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping.ContextQuery;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.elasticsearch.search.suggest.SuggestUtils.parseSuggestContext;
|
import static org.elasticsearch.search.suggest.SuggestUtils.parseSuggestContext;
|
||||||
|
|
||||||
|
@ -47,6 +53,9 @@ public class CompletionSuggestParser implements SuggestContextParser {
|
||||||
XContentParser.Token token;
|
XContentParser.Token token;
|
||||||
String fieldName = null;
|
String fieldName = null;
|
||||||
CompletionSuggestionContext suggestion = new CompletionSuggestionContext(completionSuggester);
|
CompletionSuggestionContext suggestion = new CompletionSuggestionContext(completionSuggester);
|
||||||
|
|
||||||
|
XContentParser contextParser = null;
|
||||||
|
|
||||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||||
if (token == XContentParser.Token.FIELD_NAME) {
|
if (token == XContentParser.Token.FIELD_NAME) {
|
||||||
fieldName = parser.currentName();
|
fieldName = parser.currentName();
|
||||||
|
@ -56,7 +65,8 @@ public class CompletionSuggestParser implements SuggestContextParser {
|
||||||
suggestion.setFuzzy(parser.booleanValue());
|
suggestion.setFuzzy(parser.booleanValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (token == XContentParser.Token.START_OBJECT && "fuzzy".equals(fieldName)) {
|
} else if (token == XContentParser.Token.START_OBJECT) {
|
||||||
|
if("fuzzy".equals(fieldName)) {
|
||||||
suggestion.setFuzzy(true);
|
suggestion.setFuzzy(true);
|
||||||
String fuzzyConfigName = null;
|
String fuzzyConfigName = null;
|
||||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||||
|
@ -76,12 +86,36 @@ public class CompletionSuggestParser implements SuggestContextParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if("context".equals(fieldName)) {
|
||||||
|
// Copy the current structure. We will parse, once the mapping is provided
|
||||||
|
XContentBuilder builder = XContentFactory.contentBuilder(parser.contentType());
|
||||||
|
builder.copyCurrentStructure(parser);
|
||||||
|
BytesReference bytes = builder.bytes();
|
||||||
|
contextParser = parser.contentType().xContent().createParser(bytes);
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchIllegalArgumentException("suggester [completion] doesn't support field [" + fieldName + "]");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new ElasticsearchIllegalArgumentException("suggester[completion] doesn't support field [" + fieldName + "]");
|
throw new ElasticsearchIllegalArgumentException("suggester[completion] doesn't support field [" + fieldName + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suggestion.mapper(mapperService.smartNameFieldMapper(suggestion.getField()));
|
|
||||||
|
|
||||||
|
suggestion.mapper((CompletionFieldMapper)mapperService.smartNameFieldMapper(suggestion.getField()));
|
||||||
|
|
||||||
|
CompletionFieldMapper mapper = suggestion.mapper();
|
||||||
|
if (mapper != null) {
|
||||||
|
if (mapper.requiresContext()) {
|
||||||
|
if (contextParser == null) {
|
||||||
|
throw new ElasticsearchIllegalArgumentException("suggester [completion] requires context to be setup");
|
||||||
|
} else {
|
||||||
|
contextParser.nextToken();
|
||||||
|
List<ContextQuery> contextQueries = ContextQuery.parseQueries(mapper.getContextMapping(), contextParser);
|
||||||
|
suggestion.setContextQuery(contextQueries);
|
||||||
|
}
|
||||||
|
} else if (contextParser != null) {
|
||||||
|
throw new ElasticsearchIllegalArgumentException("suggester [completion] doesn't expect any context");
|
||||||
|
}
|
||||||
|
}
|
||||||
return suggestion;
|
return suggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,32 +19,37 @@
|
||||||
package org.elasticsearch.search.suggest.completion;
|
package org.elasticsearch.search.suggest.completion;
|
||||||
|
|
||||||
import org.apache.lucene.search.suggest.analyzing.XFuzzySuggester;
|
import org.apache.lucene.search.suggest.analyzing.XFuzzySuggester;
|
||||||
import org.elasticsearch.index.mapper.FieldMapper;
|
import org.elasticsearch.index.mapper.core.CompletionFieldMapper;
|
||||||
import org.elasticsearch.search.suggest.Suggester;
|
import org.elasticsearch.search.suggest.Suggester;
|
||||||
import org.elasticsearch.search.suggest.SuggestionSearchContext;
|
import org.elasticsearch.search.suggest.SuggestionSearchContext;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping.ContextQuery;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class CompletionSuggestionContext extends SuggestionSearchContext.SuggestionContext {
|
public class CompletionSuggestionContext extends SuggestionSearchContext.SuggestionContext {
|
||||||
|
|
||||||
private FieldMapper<?> mapper;
|
private CompletionFieldMapper mapper;
|
||||||
private int fuzzyEditDistance = XFuzzySuggester.DEFAULT_MAX_EDITS;
|
private int fuzzyEditDistance = XFuzzySuggester.DEFAULT_MAX_EDITS;
|
||||||
private boolean fuzzyTranspositions = XFuzzySuggester.DEFAULT_TRANSPOSITIONS;
|
private boolean fuzzyTranspositions = XFuzzySuggester.DEFAULT_TRANSPOSITIONS;
|
||||||
private int fuzzyMinLength = XFuzzySuggester.DEFAULT_MIN_FUZZY_LENGTH;
|
private int fuzzyMinLength = XFuzzySuggester.DEFAULT_MIN_FUZZY_LENGTH;
|
||||||
private int fuzzyPrefixLength = XFuzzySuggester.DEFAULT_NON_FUZZY_PREFIX;
|
private int fuzzyPrefixLength = XFuzzySuggester.DEFAULT_NON_FUZZY_PREFIX;
|
||||||
private boolean fuzzy = false;
|
private boolean fuzzy = false;
|
||||||
private boolean fuzzyUnicodeAware = XFuzzySuggester.DEFAULT_UNICODE_AWARE;
|
private boolean fuzzyUnicodeAware = XFuzzySuggester.DEFAULT_UNICODE_AWARE;
|
||||||
|
private List<ContextQuery> contextQueries = Collections.emptyList();
|
||||||
|
|
||||||
public CompletionSuggestionContext(Suggester suggester) {
|
public CompletionSuggestionContext(Suggester suggester) {
|
||||||
super(suggester);
|
super(suggester);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FieldMapper<?> mapper() {
|
public CompletionFieldMapper mapper() {
|
||||||
return this.mapper;
|
return this.mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mapper(FieldMapper<?> mapper) {
|
public void mapper(CompletionFieldMapper mapper) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,4 +100,12 @@ public class CompletionSuggestionContext extends SuggestionSearchContext.Suggest
|
||||||
public boolean isFuzzyUnicodeAware() {
|
public boolean isFuzzyUnicodeAware() {
|
||||||
return fuzzyUnicodeAware;
|
return fuzzyUnicodeAware;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setContextQuery(List<ContextQuery> queries) {
|
||||||
|
this.contextQueries = queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ContextQuery> getContextQueries() {
|
||||||
|
return this.contextQueries;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,341 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.search.suggest.context;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import org.apache.lucene.analysis.PrefixAnalyzer;
|
||||||
|
import org.apache.lucene.analysis.TokenStream;
|
||||||
|
import org.apache.lucene.index.IndexableField;
|
||||||
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
|
import org.apache.lucene.util.automaton.BasicAutomata;
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext.Document;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link CategoryContextMapping} is used to define a {@link ContextMapping} that
|
||||||
|
* references a field within a document. The value of the field in turn will be
|
||||||
|
* used to setup the suggestions made by the completion suggester.
|
||||||
|
*/
|
||||||
|
public class CategoryContextMapping extends ContextMapping {
|
||||||
|
|
||||||
|
protected static final String TYPE = "field";
|
||||||
|
|
||||||
|
private static final String FIELD_FIELDNAME = "path";
|
||||||
|
private static final String DEFAULT_FIELDNAME = "_type";
|
||||||
|
|
||||||
|
private static final Iterable<? extends CharSequence> EMPTY_VALUES = Collections.emptyList();
|
||||||
|
|
||||||
|
private final String fieldName;
|
||||||
|
private final Iterable<? extends CharSequence> defaultValues;
|
||||||
|
private final FieldConfig defaultConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryContextMapping} with the default field
|
||||||
|
* <code>[_type]</code>
|
||||||
|
*/
|
||||||
|
public CategoryContextMapping(String name) {
|
||||||
|
this(name, DEFAULT_FIELDNAME, EMPTY_VALUES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryContextMapping} with the default field
|
||||||
|
* <code>[_type]</code>
|
||||||
|
*/
|
||||||
|
public CategoryContextMapping(String name, String fieldName) {
|
||||||
|
this(name, fieldName, EMPTY_VALUES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryContextMapping} with the default field
|
||||||
|
* <code>[_type]</code>
|
||||||
|
*/
|
||||||
|
public CategoryContextMapping(String name, Iterable<? extends CharSequence> defaultValues) {
|
||||||
|
this(name, DEFAULT_FIELDNAME, defaultValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryContextMapping} with the default field
|
||||||
|
* <code>[_type]</code>
|
||||||
|
*/
|
||||||
|
public CategoryContextMapping(String name, String fieldName, Iterable<? extends CharSequence> defaultValues) {
|
||||||
|
super(TYPE, name);
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
this.defaultValues = defaultValues;
|
||||||
|
this.defaultConfig = new FieldConfig(fieldName, defaultValues, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the field used by this {@link CategoryContextMapping}
|
||||||
|
*/
|
||||||
|
public String getFieldName() {
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<? extends CharSequence> getDefaultValues() {
|
||||||
|
return defaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FieldConfig defaultConfig() {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the specification of a {@link CategoryContextMapping}
|
||||||
|
*
|
||||||
|
* @param field
|
||||||
|
* name of the field to use. If <code>null</code> default field
|
||||||
|
* will be used
|
||||||
|
* @return new {@link CategoryContextMapping}
|
||||||
|
*/
|
||||||
|
protected static CategoryContextMapping load(String name, Map<String, Object> config) throws ElasticsearchParseException {
|
||||||
|
CategoryContextMapping.Builder mapping = new CategoryContextMapping.Builder(name);
|
||||||
|
|
||||||
|
Object fieldName = config.get(FIELD_FIELDNAME);
|
||||||
|
Object defaultValues = config.get(FIELD_MISSING);
|
||||||
|
|
||||||
|
if (fieldName != null) {
|
||||||
|
mapping.fieldName(fieldName.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValues != null) {
|
||||||
|
if (defaultValues instanceof Iterable) {
|
||||||
|
for (Object value : (Iterable) defaultValues) {
|
||||||
|
mapping.addDefaultValue(value.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mapping.addDefaultValue(defaultValues.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
if (fieldName != null) {
|
||||||
|
builder.field(FIELD_FIELDNAME, fieldName);
|
||||||
|
}
|
||||||
|
builder.startArray(FIELD_MISSING);
|
||||||
|
for (CharSequence value : defaultValues) {
|
||||||
|
builder.value(value);
|
||||||
|
}
|
||||||
|
builder.endArray();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContextConfig parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException {
|
||||||
|
Token token = parser.currentToken();
|
||||||
|
if (token == Token.VALUE_NULL) {
|
||||||
|
return new FieldConfig(fieldName, defaultValues, null);
|
||||||
|
} else if (token == Token.VALUE_STRING) {
|
||||||
|
return new FieldConfig(fieldName, null, Collections.singleton(parser.text()));
|
||||||
|
} else if (token == Token.VALUE_NUMBER) {
|
||||||
|
return new FieldConfig(fieldName, null, Collections.singleton(parser.text()));
|
||||||
|
} else if (token == Token.VALUE_BOOLEAN) {
|
||||||
|
return new FieldConfig(fieldName, null, Collections.singleton(parser.text()));
|
||||||
|
} else if (token == Token.START_ARRAY) {
|
||||||
|
ArrayList<String> values = Lists.newArrayList();
|
||||||
|
while((token = parser.nextToken()) != Token.END_ARRAY) {
|
||||||
|
values.add(parser.text());
|
||||||
|
}
|
||||||
|
if(values.isEmpty()) {
|
||||||
|
throw new ElasticsearchParseException("FieldConfig must contain a least one category");
|
||||||
|
}
|
||||||
|
return new FieldConfig(fieldName, null, values);
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("FieldConfig must be either [null], a string or a list of strings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FieldQuery parseQuery(String name, XContentParser parser) throws IOException, ElasticsearchParseException {
|
||||||
|
Iterable<? extends CharSequence> values;
|
||||||
|
Token token = parser.currentToken();
|
||||||
|
if (token == Token.START_ARRAY) {
|
||||||
|
ArrayList<String> list = new ArrayList<String>();
|
||||||
|
while ((token = parser.nextToken()) != Token.END_ARRAY) {
|
||||||
|
list.add(parser.text());
|
||||||
|
}
|
||||||
|
values = list;
|
||||||
|
} else if (token == Token.VALUE_NULL) {
|
||||||
|
values = defaultValues;
|
||||||
|
} else {
|
||||||
|
values = Collections.singleton(parser.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FieldQuery(name, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FieldQuery query(String name, CharSequence... fieldvalues) {
|
||||||
|
return query(name, Arrays.asList(fieldvalues));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FieldQuery query(String name, Iterable<? extends CharSequence> fieldvalues) {
|
||||||
|
return new FieldQuery(name, fieldvalues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof CategoryContextMapping) {
|
||||||
|
CategoryContextMapping other = (CategoryContextMapping) obj;
|
||||||
|
if (this.fieldName.equals(other.fieldName)) {
|
||||||
|
return Iterables.elementsEqual(this.defaultValues, other.defaultValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FieldConfig extends ContextConfig {
|
||||||
|
|
||||||
|
private final String fieldname;
|
||||||
|
private final Iterable<? extends CharSequence> defaultValues;
|
||||||
|
private final Iterable<? extends CharSequence> values;
|
||||||
|
|
||||||
|
public FieldConfig(String fieldname, Iterable<? extends CharSequence> defaultValues, Iterable<? extends CharSequence> values) {
|
||||||
|
this.fieldname = fieldname;
|
||||||
|
this.defaultValues = defaultValues;
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TokenStream wrapTokenStream(Document doc, TokenStream stream) {
|
||||||
|
if(values != null) {
|
||||||
|
return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, values);
|
||||||
|
} else {
|
||||||
|
IndexableField[] fields = doc.getFields(fieldname);
|
||||||
|
ArrayList<CharSequence> values = new ArrayList<CharSequence>(fields.length);
|
||||||
|
|
||||||
|
for (int i = 0; i < fields.length; i++) {
|
||||||
|
values.add(fields[i].stringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder("FieldConfig(" + fieldname + " = [");
|
||||||
|
Iterator<? extends CharSequence> value = this.defaultValues.iterator();
|
||||||
|
if (value.hasNext()) {
|
||||||
|
sb.append(value.next());
|
||||||
|
while (value.hasNext()) {
|
||||||
|
sb.append(", ").append(value.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.append("])").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FieldQuery extends ContextQuery {
|
||||||
|
|
||||||
|
private final Iterable<? extends CharSequence> values;
|
||||||
|
|
||||||
|
public FieldQuery(String name, Iterable<? extends CharSequence> values) {
|
||||||
|
super(name);
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Automaton toAutomaton() {
|
||||||
|
List<Automaton> automatons = new ArrayList<Automaton>();
|
||||||
|
for (CharSequence value : values) {
|
||||||
|
automatons.add(BasicAutomata.makeString(value.toString()));
|
||||||
|
}
|
||||||
|
return Automaton.union(automatons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
builder.startArray(name);
|
||||||
|
for (CharSequence value : values) {
|
||||||
|
builder.value(value);
|
||||||
|
}
|
||||||
|
builder.endArray();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder extends ContextBuilder<CategoryContextMapping> {
|
||||||
|
|
||||||
|
private String fieldname;
|
||||||
|
private List<CharSequence> defaultValues = new ArrayList<CharSequence>();
|
||||||
|
|
||||||
|
public Builder(String name) {
|
||||||
|
this(name, DEFAULT_FIELDNAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder(String name, String fieldname) {
|
||||||
|
super(name);
|
||||||
|
this.fieldname = fieldname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the name of the field to use
|
||||||
|
*/
|
||||||
|
public Builder fieldName(String fieldname) {
|
||||||
|
this.fieldname = fieldname;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add value to the default values of the mapping
|
||||||
|
*/
|
||||||
|
public Builder addDefaultValue(CharSequence defaultValue) {
|
||||||
|
this.defaultValues.add(defaultValue);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add set of default values to the mapping
|
||||||
|
*/
|
||||||
|
public Builder addDefaultValues(CharSequence... defaultValues) {
|
||||||
|
for (CharSequence defaultValue : defaultValues) {
|
||||||
|
this.defaultValues.add(defaultValue);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add set of default values to the mapping
|
||||||
|
*/
|
||||||
|
public Builder addDefaultValues(Iterable<? extends CharSequence> defaultValues) {
|
||||||
|
for (CharSequence defaultValue : defaultValues) {
|
||||||
|
this.defaultValues.add(defaultValue);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CategoryContextMapping build() {
|
||||||
|
return new CategoryContextMapping(name, fieldname, defaultValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.search.suggest.context;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
public abstract class ContextBuilder<E extends ContextMapping> {
|
||||||
|
|
||||||
|
protected String name;
|
||||||
|
|
||||||
|
public ContextBuilder(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract E build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link GeolocationContextMapping}
|
||||||
|
*/
|
||||||
|
public static GeolocationContextMapping.Builder location(String name) {
|
||||||
|
return new GeolocationContextMapping.Builder(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link GeolocationContextMapping} with given precision and
|
||||||
|
* neighborhood usage
|
||||||
|
*
|
||||||
|
* @param precision geohash length
|
||||||
|
* @param neighbors use neighbor cells
|
||||||
|
*/
|
||||||
|
public static GeolocationContextMapping.Builder location(String name, int precision, boolean neighbors) {
|
||||||
|
return new GeolocationContextMapping.Builder(name, neighbors, precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryMapping}
|
||||||
|
*/
|
||||||
|
public static CategoryContextMapping.Builder category(String name) {
|
||||||
|
return new CategoryContextMapping.Builder(name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryMapping} with default category
|
||||||
|
*
|
||||||
|
* @param defaultCategory category to use, if it is not provided
|
||||||
|
*/
|
||||||
|
public static CategoryContextMapping.Builder category(String name, String defaultCategory) {
|
||||||
|
return new CategoryContextMapping.Builder(name, null).addDefaultValue(defaultCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryContextMapping}
|
||||||
|
*
|
||||||
|
* @param fieldname
|
||||||
|
* name of the field to use
|
||||||
|
*/
|
||||||
|
public static CategoryContextMapping.Builder reference(String name, String fieldname) {
|
||||||
|
return new CategoryContextMapping.Builder(name, fieldname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CategoryContextMapping}
|
||||||
|
*
|
||||||
|
* @param fieldname name of the field to use
|
||||||
|
* @param defaultValues values to use, if the document not provides
|
||||||
|
* a field with the given name
|
||||||
|
*/
|
||||||
|
public static CategoryContextMapping.Builder reference(String name, String fieldname, Iterable<? extends CharSequence> defaultValues) {
|
||||||
|
return new CategoryContextMapping.Builder(name, fieldname).addDefaultValues(defaultValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SortedMap<String, ContextMapping> loadMappings(Object configuration) throws ElasticsearchParseException {
|
||||||
|
if (configuration instanceof Map) {
|
||||||
|
Map<String, Object> configurations = (Map<String, Object>)configuration;
|
||||||
|
SortedMap<String, ContextMapping> mappings = Maps.newTreeMap();
|
||||||
|
for (Entry<String,Object> config : configurations.entrySet()) {
|
||||||
|
String name = config.getKey();
|
||||||
|
mappings.put(name, loadMapping(name, (Map<String, Object>) config.getValue()));
|
||||||
|
}
|
||||||
|
return mappings;
|
||||||
|
} else if (configuration == null) {
|
||||||
|
return ContextMapping.EMPTY_MAPPING;
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("no valid context configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ContextMapping loadMapping(String name, Map<String, Object> config) throws ElasticsearchParseException {
|
||||||
|
final Object argType = config.get(ContextMapping.FIELD_TYPE);
|
||||||
|
|
||||||
|
if (argType == null) {
|
||||||
|
throw new ElasticsearchParseException("missing [" + ContextMapping.FIELD_TYPE + "] in context mapping");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String type = argType.toString();
|
||||||
|
|
||||||
|
if (GeolocationContextMapping.TYPE.equals(type)) {
|
||||||
|
return GeolocationContextMapping.load(name, config);
|
||||||
|
} else if (CategoryContextMapping.TYPE.equals(type)) {
|
||||||
|
return CategoryContextMapping.load(name, config);
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("unknown context type[" + type + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,316 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.search.suggest.context;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import org.apache.lucene.analysis.TokenStream;
|
||||||
|
import org.apache.lucene.search.suggest.analyzing.XAnalyzingSuggester;
|
||||||
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
|
import org.apache.lucene.util.automaton.BasicAutomata;
|
||||||
|
import org.apache.lucene.util.automaton.BasicOperations;
|
||||||
|
import org.apache.lucene.util.fst.FST;
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.common.xcontent.ToXContent;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||||
|
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext.Document;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ContextMapping} is used t define a context that may used
|
||||||
|
* in conjunction with a suggester. To define a suggester that depends on a
|
||||||
|
* specific context derived class of {@link ContextMapping} will be
|
||||||
|
* used to specify the kind of additional information required in order to make
|
||||||
|
* suggestions.
|
||||||
|
*/
|
||||||
|
public abstract class ContextMapping implements ToXContent {
|
||||||
|
|
||||||
|
/** Character used to separate several contexts */
|
||||||
|
public static final char SEPARATOR = '\u001D';
|
||||||
|
|
||||||
|
/** Dummy Context Mapping that should be used if no context is used*/
|
||||||
|
public static final SortedMap<String, ContextMapping> EMPTY_MAPPING = Maps.newTreeMap();
|
||||||
|
|
||||||
|
/** Dummy Context Config matching the Dummy Mapping by providing an empty context*/
|
||||||
|
public static final SortedMap<String, ContextConfig> EMPTY_CONFIG = Maps.newTreeMap();
|
||||||
|
|
||||||
|
/** Dummy Context matching the Dummy Mapping by not wrapping a {@link TokenStream} */
|
||||||
|
public static final Context EMPTY_CONTEXT = new Context(EMPTY_CONFIG, null);
|
||||||
|
|
||||||
|
public static final String FIELD_VALUE = "value";
|
||||||
|
public static final String FIELD_MISSING = "default";
|
||||||
|
public static final String FIELD_TYPE = "type";
|
||||||
|
|
||||||
|
protected final String type; // Type of the Contextmapping
|
||||||
|
protected final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a new context mapping of a specific type
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* name of the new context mapping
|
||||||
|
*/
|
||||||
|
protected ContextMapping(String type, String name) {
|
||||||
|
super();
|
||||||
|
this.type = type;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the type name of the context
|
||||||
|
*/
|
||||||
|
protected String type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the name/id of the context
|
||||||
|
*/
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
builder.startObject(name);
|
||||||
|
builder.field(FIELD_TYPE, type);
|
||||||
|
toInnerXContent(builder, params);
|
||||||
|
builder.endObject();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ContextMapping} combined with the information provided by a document
|
||||||
|
* form a {@link ContextConfig} which is used to build the underlying FST.
|
||||||
|
*
|
||||||
|
* @param parseContext context of parsing phase
|
||||||
|
* @param parser {@link XContentParser} used to read and setup the configuration
|
||||||
|
* @return A {@link ContextConfig} related to <b>this</b> mapping
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ElasticsearchParseException
|
||||||
|
*/
|
||||||
|
public abstract ContextConfig parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException;
|
||||||
|
|
||||||
|
public abstract ContextConfig defaultConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a query according to the context. Parsing starts at parsers <b>current</b> position
|
||||||
|
*
|
||||||
|
* @param name name of the context
|
||||||
|
* @param parser {@link XContentParser} providing the data of the query
|
||||||
|
*
|
||||||
|
* @return {@link ContextQuery} according to this mapping
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ElasticsearchParseException
|
||||||
|
*/
|
||||||
|
public abstract ContextQuery parseQuery(String name, XContentParser parser) throws IOException, ElasticsearchParseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since every context mapping is assumed to have a name given by the field name of an context object, this
|
||||||
|
* method is used to build the value used to serialize the mapping
|
||||||
|
*
|
||||||
|
* @param builder builder to append the mapping to
|
||||||
|
* @param params parameters passed to the builder
|
||||||
|
*
|
||||||
|
* @return the builder used
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
protected abstract XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test equality of two mapping
|
||||||
|
*
|
||||||
|
* @param thisMappings first mapping
|
||||||
|
* @param otherMappings second mapping
|
||||||
|
*
|
||||||
|
* @return true if both arguments are equal
|
||||||
|
*/
|
||||||
|
public static boolean mappingsAreEqual(SortedMap<String, ? extends ContextMapping> thisMappings, SortedMap<String, ? extends ContextMapping> otherMappings) {
|
||||||
|
return Iterables.elementsEqual(thisMappings.entrySet(), otherMappings.entrySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
return toXContent(JsonXContent.contentBuilder(), ToXContent.EMPTY_PARAMS).string();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return super.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of {@link ContextMapping}s, their {@link ContextConfig}uration and a
|
||||||
|
* Document form a complete {@link Context}. Since this Object provides all information used
|
||||||
|
* to setup a suggestion, it can be used to wrap the entire {@link TokenStream} used to build a
|
||||||
|
* path within the {@link FST}.
|
||||||
|
*/
|
||||||
|
public static class Context {
|
||||||
|
|
||||||
|
final SortedMap<String, ContextConfig> contexts;
|
||||||
|
final Document doc;
|
||||||
|
|
||||||
|
public Context(SortedMap<String, ContextConfig> contexts, Document doc) {
|
||||||
|
super();
|
||||||
|
this.contexts = contexts;
|
||||||
|
this.doc = doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the {@link TokenStream} according to the provided informations of {@link ContextConfig}
|
||||||
|
* and a related {@link Document}.
|
||||||
|
*
|
||||||
|
* @param tokenStream {@link TokenStream} to wrap
|
||||||
|
*
|
||||||
|
* @return wrapped token stream
|
||||||
|
*/
|
||||||
|
public TokenStream wrapTokenStream(TokenStream tokenStream) {
|
||||||
|
for (ContextConfig context : contexts.values()) {
|
||||||
|
tokenStream = context.wrapTokenStream(doc, tokenStream);
|
||||||
|
}
|
||||||
|
return tokenStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ContextMapping} combined with the information provided by a document
|
||||||
|
* form a {@link ContextConfig} which is used to build the underlying {@link FST}. This class hold
|
||||||
|
* a simple method wrapping a {@link TokenStream} by provided document informations.
|
||||||
|
*/
|
||||||
|
public static abstract class ContextConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a {@link TokenStream} for building suggestions to use context informations
|
||||||
|
* provided by a document or a {@link ContextMapping}
|
||||||
|
*
|
||||||
|
* @param doc document related to the stream
|
||||||
|
* @param stream original stream used to build the underlying {@link FST}
|
||||||
|
*
|
||||||
|
* @return A new {@link TokenStream} providing additional context information
|
||||||
|
*/
|
||||||
|
protected abstract TokenStream wrapTokenStream(Document doc, TokenStream stream);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ContextQuery} defines the context information for a specific {@link ContextMapping}
|
||||||
|
* defined within a suggestion request. According to the parameters set in the request and the
|
||||||
|
* {@link ContextMapping} such a query is used to wrap the {@link TokenStream} of the actual
|
||||||
|
* suggestion request into a {@link TokenStream} with the context settings
|
||||||
|
*/
|
||||||
|
public static abstract class ContextQuery implements ToXContent {
|
||||||
|
|
||||||
|
protected final String name;
|
||||||
|
|
||||||
|
protected ContextQuery(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a automaton for a given context query this automaton will be used
|
||||||
|
* to find the matching paths with the fst
|
||||||
|
*
|
||||||
|
* @param preserveSep set an additional char (<code>XAnalyzingSuggester.SEP_LABEL</code>) between each context query
|
||||||
|
* @param queries list of {@link ContextQuery} defining the lookup context
|
||||||
|
*
|
||||||
|
* @return Automaton matching the given Query
|
||||||
|
*/
|
||||||
|
public static Automaton toAutomaton(boolean preserveSep, Iterable<ContextQuery> queries) {
|
||||||
|
Automaton a = BasicAutomata.makeEmptyString();
|
||||||
|
|
||||||
|
Automaton gap = BasicAutomata.makeChar(ContextMapping.SEPARATOR);
|
||||||
|
if (preserveSep) {
|
||||||
|
// if separators are preserved the fst contains a SEP_LABEL
|
||||||
|
// behind each gap. To have a matching automaton, we need to
|
||||||
|
// include the SEP_LABEL in the query as well
|
||||||
|
gap = BasicOperations.concatenate(gap, BasicAutomata.makeChar(XAnalyzingSuggester.SEP_LABEL));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ContextQuery query : queries) {
|
||||||
|
a = Automaton.concatenate(Arrays.asList(query.toAutomaton(), gap, a));
|
||||||
|
}
|
||||||
|
BasicOperations.determinize(a);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a LookUp Automaton for this context.
|
||||||
|
* @return LookUp Automaton
|
||||||
|
*/
|
||||||
|
protected abstract Automaton toAutomaton();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a set of {@link ContextQuery} according to a given mapping
|
||||||
|
* @param mappings List of mapping defined y the suggest field
|
||||||
|
* @param parser parser holding the settings of the queries. The parsers
|
||||||
|
* current token is assumed hold an array. The number of elements
|
||||||
|
* in this array must match the number of elements in the mappings.
|
||||||
|
* @return List of context queries
|
||||||
|
*
|
||||||
|
* @throws IOException if something unexpected happened on the underlying stream
|
||||||
|
* @throws ElasticsearchParseException if the list of queries could not be parsed
|
||||||
|
*/
|
||||||
|
public static List<ContextQuery> parseQueries(Map<String, ContextMapping> mappings, XContentParser parser)
|
||||||
|
throws IOException, ElasticsearchParseException {
|
||||||
|
|
||||||
|
Map<String, ContextQuery> querySet = new HashMap<String, ContextMapping.ContextQuery>();
|
||||||
|
Token token = parser.currentToken();
|
||||||
|
if(token == Token.START_OBJECT) {
|
||||||
|
while ((token = parser.nextToken()) != Token.END_OBJECT) {
|
||||||
|
String name = parser.text();
|
||||||
|
ContextMapping mapping = mappings.get(name);
|
||||||
|
if (mapping == null) {
|
||||||
|
throw new ElasticsearchParseException("no mapping defined for [" + name + "]");
|
||||||
|
}
|
||||||
|
parser.nextToken();
|
||||||
|
querySet.put(name, mapping.parseQuery(name, parser));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ContextQuery> queries = Lists.newArrayListWithExpectedSize(mappings.size());
|
||||||
|
for (ContextMapping mapping : mappings.values()) {
|
||||||
|
queries.add(querySet.get(mapping.name));
|
||||||
|
}
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
try {
|
||||||
|
return toXContent(JsonXContent.contentBuilder(), ToXContent.EMPTY_PARAMS).string();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return super.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,664 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.search.suggest.context;
|
||||||
|
|
||||||
|
import com.carrotsearch.hppc.IntOpenHashSet;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import org.apache.lucene.analysis.PrefixAnalyzer.PrefixTokenFilter;
|
||||||
|
import org.apache.lucene.analysis.TokenStream;
|
||||||
|
import org.apache.lucene.index.IndexableField;
|
||||||
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
|
import org.apache.lucene.util.automaton.BasicAutomata;
|
||||||
|
import org.apache.lucene.util.automaton.BasicOperations;
|
||||||
|
import org.apache.lucene.util.fst.FST;
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.common.geo.GeoHashUtils;
|
||||||
|
import org.elasticsearch.common.geo.GeoPoint;
|
||||||
|
import org.elasticsearch.common.geo.GeoUtils;
|
||||||
|
import org.elasticsearch.common.unit.DistanceUnit;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||||
|
import org.elasticsearch.index.mapper.FieldMapper;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext.Document;
|
||||||
|
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link GeolocationContextMapping} allows to take GeoInfomation into account
|
||||||
|
* during building suggestions. The mapping itself works with geohashes
|
||||||
|
* explicitly and is configured by three parameters:
|
||||||
|
* <ul>
|
||||||
|
* <li><code>precision</code>: length of the geohash indexed as prefix of the
|
||||||
|
* completion field</li>
|
||||||
|
* <li><code>neighbors</code>: Should the neighbor cells of the deepest geohash
|
||||||
|
* level also be indexed as alternatives to the actual geohash</li>
|
||||||
|
* <li><code>location</code>: (optional) location assumed if it is not provided</li>
|
||||||
|
* </ul>
|
||||||
|
* Internally this mapping wraps the suggestions into a form
|
||||||
|
* <code>[geohash][suggestion]</code>. If the neighbor option is set the cells
|
||||||
|
* next to the cell on the deepest geohash level ( <code>precision</code>) will
|
||||||
|
* be indexed as well. The {@link TokenStream} used to build the {@link FST} for
|
||||||
|
* suggestion will be wrapped into a {@link PrefixTokenFilter} managing these
|
||||||
|
* geohases as prefixes.
|
||||||
|
*/
|
||||||
|
public class GeolocationContextMapping extends ContextMapping {
|
||||||
|
|
||||||
|
public static final String TYPE = "geo";
|
||||||
|
|
||||||
|
public static final String FIELD_PRECISION = "precision";
|
||||||
|
public static final String FIELD_NEIGHBORS = "neighbors";
|
||||||
|
public static final String FIELD_FIELDNAME = "path";
|
||||||
|
|
||||||
|
private final Collection<String> defaultLocations;
|
||||||
|
private final int[] precision;
|
||||||
|
private final boolean neighbors;
|
||||||
|
private final String fieldName;
|
||||||
|
private final GeoConfig defaultConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link GeolocationContextMapping} with a given precision
|
||||||
|
*
|
||||||
|
* @param precision
|
||||||
|
* length of the geohashes
|
||||||
|
* @param neighbors
|
||||||
|
* should neighbors be indexed
|
||||||
|
* @param defaultLocation
|
||||||
|
* location to use, if it is not provided by the document
|
||||||
|
*/
|
||||||
|
protected GeolocationContextMapping(String name, int[] precision, boolean neighbors, Collection<String> defaultLocations, String fieldName) {
|
||||||
|
super(TYPE, name);
|
||||||
|
this.precision = precision;
|
||||||
|
this.neighbors = neighbors;
|
||||||
|
this.defaultLocations = defaultLocations;
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
this.defaultConfig = new GeoConfig(this, defaultLocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load a {@link GeolocationContextMapping} by configuration. Such a configuration
|
||||||
|
* can set the parameters
|
||||||
|
* <ul>
|
||||||
|
* <li>precision [<code>String</code>, <code>Double</code>,
|
||||||
|
* <code>Float</code> or <code>Integer</code>] defines the length of the
|
||||||
|
* underlying geohash</li>
|
||||||
|
* <li>defaultLocation [<code>String</code>] defines the location to use if
|
||||||
|
* it is not provided by the document</li>
|
||||||
|
* <li>neighbors [<code>Boolean</code>] defines if the last level of the
|
||||||
|
* geohash should be extended by neighbor cells</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
* Configuration for {@link GeolocationContextMapping}
|
||||||
|
* @return new {@link GeolocationContextMapping} configured by the parameters of
|
||||||
|
* <code>config</code>
|
||||||
|
*/
|
||||||
|
protected static GeolocationContextMapping load(String name, Map<String, Object> config) {
|
||||||
|
final GeolocationContextMapping.Builder builder = new GeolocationContextMapping.Builder(name);
|
||||||
|
|
||||||
|
if (config != null) {
|
||||||
|
final Object configPrecision = config.get(FIELD_PRECISION);
|
||||||
|
if (configPrecision == null) {
|
||||||
|
// ignore precision
|
||||||
|
} else if (configPrecision instanceof Integer) {
|
||||||
|
builder.precision((Integer) configPrecision);
|
||||||
|
} else if (configPrecision instanceof Long) {
|
||||||
|
builder.precision((Long) configPrecision);
|
||||||
|
} else if (configPrecision instanceof Double) {
|
||||||
|
builder.precision((Double) configPrecision);
|
||||||
|
} else if (configPrecision instanceof Float) {
|
||||||
|
builder.precision((Float) configPrecision);
|
||||||
|
} else if (configPrecision instanceof Iterable) {
|
||||||
|
for (Object precision : (Iterable)configPrecision) {
|
||||||
|
if (precision instanceof Integer) {
|
||||||
|
builder.precision((Integer) precision);
|
||||||
|
} else if (precision instanceof Long) {
|
||||||
|
builder.precision((Long) precision);
|
||||||
|
} else if (precision instanceof Double) {
|
||||||
|
builder.precision((Double) precision);
|
||||||
|
} else if (precision instanceof Float) {
|
||||||
|
builder.precision((Float) precision);
|
||||||
|
} else {
|
||||||
|
builder.precision(precision.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.precision(configPrecision.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object configNeighbors = config.get(FIELD_NEIGHBORS);
|
||||||
|
if (configNeighbors != null) {
|
||||||
|
builder.neighbors((Boolean) configNeighbors);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object def = config.get(FIELD_MISSING);
|
||||||
|
if (def != null) {
|
||||||
|
if (def instanceof Iterable) {
|
||||||
|
for (Object location : (Iterable)def) {
|
||||||
|
builder.addDefaultLocation(location.toString());
|
||||||
|
}
|
||||||
|
} else if (def instanceof String) {
|
||||||
|
builder.addDefaultLocation(def.toString());
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("field [" + FIELD_MISSING + "] must be of type string or list");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object fieldName = config.get(FIELD_FIELDNAME);
|
||||||
|
if (fieldName != null) {
|
||||||
|
builder.field(fieldName.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
builder.field(FIELD_PRECISION, precision);
|
||||||
|
builder.field(FIELD_NEIGHBORS, neighbors);
|
||||||
|
if (defaultLocations != null) {
|
||||||
|
builder.startArray(FIELD_MISSING);
|
||||||
|
for (String defaultLocation : defaultLocations) {
|
||||||
|
builder.value(defaultLocation);
|
||||||
|
}
|
||||||
|
builder.endArray();
|
||||||
|
}
|
||||||
|
if (fieldName != null) {
|
||||||
|
builder.field(FIELD_FIELDNAME, fieldName);
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Collection<String> parseSinglePointOrList(XContentParser parser) throws IOException {
|
||||||
|
Token token = parser.currentToken();
|
||||||
|
if(token == Token.START_ARRAY) {
|
||||||
|
token = parser.nextToken();
|
||||||
|
// Test if value is a single point in <code>[lon, lat]</code> format
|
||||||
|
if(token == Token.VALUE_NUMBER) {
|
||||||
|
double lon = parser.doubleValue();
|
||||||
|
if(parser.nextToken() == Token.VALUE_NUMBER) {
|
||||||
|
double lat = parser.doubleValue();
|
||||||
|
if(parser.nextToken() == Token.END_ARRAY) {
|
||||||
|
return Collections.singleton(GeoHashUtils.encode(lat, lon));
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("only two values expected");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("latitue must be a numeric value");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise it's a list of locations
|
||||||
|
ArrayList<String> result = Lists.newArrayList();
|
||||||
|
while (token != Token.END_ARRAY) {
|
||||||
|
result.add(GeoPoint.parse(parser).geohash());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// or a single location
|
||||||
|
return Collections.singleton(GeoPoint.parse(parser).geohash());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContextConfig defaultConfig() {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContextConfig parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException {
|
||||||
|
|
||||||
|
if(fieldName != null) {
|
||||||
|
FieldMapper<?> mapper = parseContext.docMapper().mappers().fullName(fieldName).mapper();
|
||||||
|
if(!(mapper instanceof GeoPointFieldMapper)) {
|
||||||
|
throw new ElasticsearchParseException("referenced field must be mapped to geo_point");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<String> locations;
|
||||||
|
if(parser.currentToken() == Token.VALUE_NULL) {
|
||||||
|
locations = null;
|
||||||
|
} else {
|
||||||
|
locations = parseSinglePointOrList(parser);
|
||||||
|
}
|
||||||
|
return new GeoConfig(this, locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new geolocation query from a given GeoPoint
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* query location
|
||||||
|
* @return new geolocation query
|
||||||
|
*/
|
||||||
|
public static GeoQuery query(String name, GeoPoint point) {
|
||||||
|
return query(name, point.getGeohash());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new geolocation query from a given geocoordinate
|
||||||
|
*
|
||||||
|
* @param lat
|
||||||
|
* latitude of the location
|
||||||
|
* @param lon
|
||||||
|
* longitude of the location
|
||||||
|
* @return new geolocation query
|
||||||
|
*/
|
||||||
|
public static GeoQuery query(String name, double lat, double lon) {
|
||||||
|
return query(name, GeoHashUtils.encode(lat, lon));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new geolocation query from a given geohash
|
||||||
|
*
|
||||||
|
* @param geohash
|
||||||
|
* geohash of the location
|
||||||
|
* @return new geolocation query
|
||||||
|
*/
|
||||||
|
public static GeoQuery query(String name, String geohash) {
|
||||||
|
return new GeoQuery(name, geohash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int parsePrecision(XContentParser parser) throws IOException, ElasticsearchParseException {
|
||||||
|
switch (parser.currentToken()) {
|
||||||
|
case VALUE_STRING:
|
||||||
|
return GeoUtils.geoHashLevelsForPrecision(parser.text());
|
||||||
|
case VALUE_NUMBER:
|
||||||
|
switch (parser.numberType()) {
|
||||||
|
case INT:
|
||||||
|
case LONG:
|
||||||
|
return parser.intValue();
|
||||||
|
default:
|
||||||
|
return GeoUtils.geoHashLevelsForPrecision(parser.doubleValue());
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new ElasticsearchParseException("invalid precision value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeoQuery parseQuery(String name, XContentParser parser) throws IOException, ElasticsearchParseException {
|
||||||
|
if (parser.currentToken() == Token.START_OBJECT) {
|
||||||
|
double lat = Double.NaN;
|
||||||
|
double lon = Double.NaN;
|
||||||
|
GeoPoint point = null;
|
||||||
|
int[] precision = null;
|
||||||
|
|
||||||
|
while (parser.nextToken() != Token.END_OBJECT) {
|
||||||
|
final String fieldName = parser.text();
|
||||||
|
if("lat".equals(fieldName)) {
|
||||||
|
if(point == null) {
|
||||||
|
if (parser.nextToken() == Token.VALUE_NUMBER) {
|
||||||
|
lat = parser.doubleValue();
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("latitude must be a number");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("only lat/lon or [" + FIELD_VALUE + "] is allowed");
|
||||||
|
}
|
||||||
|
} else if ("lon".equals(fieldName)) {
|
||||||
|
if(point == null) {
|
||||||
|
if(parser.nextToken() == Token.VALUE_NUMBER) {
|
||||||
|
lon = parser.doubleValue();
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("longitude must be a number");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("only lat/lon or [" + FIELD_VALUE + "] is allowed");
|
||||||
|
}
|
||||||
|
} else if (FIELD_PRECISION.equals(fieldName)) {
|
||||||
|
if(parser.nextToken() == Token.START_ARRAY) {
|
||||||
|
IntOpenHashSet precisions = new IntOpenHashSet();
|
||||||
|
while(parser.nextToken() != Token.END_ARRAY) {
|
||||||
|
precisions.add(parsePrecision(parser));
|
||||||
|
}
|
||||||
|
precision = precisions.toArray();
|
||||||
|
} else {
|
||||||
|
precision = new int[] { parsePrecision(parser) };
|
||||||
|
}
|
||||||
|
} else if (FIELD_VALUE.equals(fieldName)) {
|
||||||
|
if(lat == Double.NaN && lon == Double.NaN) {
|
||||||
|
point = GeoPoint.parse(parser);
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("only lat/lon or [" + FIELD_VALUE + "] is allowed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("unexpected fieldname [" + fieldName + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point == null) {
|
||||||
|
if (lat == Double.NaN || lon == Double.NaN) {
|
||||||
|
throw new ElasticsearchParseException("location is missing");
|
||||||
|
} else {
|
||||||
|
point = new GeoPoint(lat, lon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GeoQuery(name, point.geohash(), precision);
|
||||||
|
} else {
|
||||||
|
return new GeoQuery(name, GeoPoint.parse(parser).getGeohash(), precision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((defaultLocations == null) ? 0 : defaultLocations.hashCode());
|
||||||
|
result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode());
|
||||||
|
result = prime * result + (neighbors ? 1231 : 1237);
|
||||||
|
result = prime * result + Arrays.hashCode(precision);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
GeolocationContextMapping other = (GeolocationContextMapping) obj;
|
||||||
|
if (defaultLocations == null) {
|
||||||
|
if (other.defaultLocations != null)
|
||||||
|
return false;
|
||||||
|
} else if (!defaultLocations.equals(other.defaultLocations))
|
||||||
|
return false;
|
||||||
|
if (fieldName == null) {
|
||||||
|
if (other.fieldName != null)
|
||||||
|
return false;
|
||||||
|
} else if (!fieldName.equals(other.fieldName))
|
||||||
|
return false;
|
||||||
|
if (neighbors != other.neighbors)
|
||||||
|
return false;
|
||||||
|
if (!Arrays.equals(precision, other.precision))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static class Builder extends ContextBuilder<GeolocationContextMapping> {
|
||||||
|
|
||||||
|
private IntOpenHashSet precisions = new IntOpenHashSet();
|
||||||
|
private boolean neighbors; // take neighbor cell on the lowest level into account
|
||||||
|
private HashSet<String> defaultLocations = new HashSet<String>();
|
||||||
|
private String fieldName = null;
|
||||||
|
|
||||||
|
protected Builder(String name) {
|
||||||
|
this(name, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Builder(String name, boolean neighbors, int...levels) {
|
||||||
|
super(name);
|
||||||
|
neighbors(neighbors);
|
||||||
|
if (levels != null) {
|
||||||
|
for (int level : levels) {
|
||||||
|
precision(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the precision use o make suggestions
|
||||||
|
*
|
||||||
|
* @param precision
|
||||||
|
* precision as distance with {@link DistanceUnit}. Default:
|
||||||
|
* meters
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder precision(String precision) {
|
||||||
|
return precision(DistanceUnit.parse(precision, DistanceUnit.METERS, DistanceUnit.METERS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the precision use o make suggestions
|
||||||
|
*
|
||||||
|
* @param precision
|
||||||
|
* precision value
|
||||||
|
* @param unit
|
||||||
|
* {@link DistanceUnit} to use
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder precision(double precision, DistanceUnit unit) {
|
||||||
|
return precision(unit.toMeters(precision));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the precision use o make suggestions
|
||||||
|
*
|
||||||
|
* @param precision
|
||||||
|
* precision as distance in meters
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder precision(double meters) {
|
||||||
|
int level = GeoUtils.geoHashLevelsForPrecision(meters);
|
||||||
|
// Ceiling precision: we might return more results
|
||||||
|
if (GeoUtils.geoHashCellSize(level) < meters) {
|
||||||
|
level = Math.max(1, level - 1);
|
||||||
|
}
|
||||||
|
return precision(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the precision use o make suggestions
|
||||||
|
*
|
||||||
|
* @param precision
|
||||||
|
* maximum length of geohashes
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder precision(int level) {
|
||||||
|
this.precisions.add(level);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set neighborhood usage
|
||||||
|
*
|
||||||
|
* @param neighbors
|
||||||
|
* should neighbor cells also be valid
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder neighbors(boolean neighbors) {
|
||||||
|
this.neighbors = neighbors;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a default location that should be used, if no location is
|
||||||
|
* provided by the query
|
||||||
|
*
|
||||||
|
* @param geohash
|
||||||
|
* geohash of the default location
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder addDefaultLocation(String geohash) {
|
||||||
|
this.defaultLocations.add(geohash);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a default location that should be used, if no location is
|
||||||
|
* provided by the query
|
||||||
|
*
|
||||||
|
* @param geohash
|
||||||
|
* geohash of the default location
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder addDefaultLocations(Collection<String> geohashes) {
|
||||||
|
this.defaultLocations.addAll(geohashes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a default location that should be used, if no location is
|
||||||
|
* provided by the query
|
||||||
|
*
|
||||||
|
* @param lat
|
||||||
|
* latitude of the default location
|
||||||
|
* @param lon
|
||||||
|
* longitude of the default location
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder addDefaultLocation(double lat, double lon) {
|
||||||
|
this.defaultLocations.add(GeoHashUtils.encode(lat, lon));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a default location that should be used, if no location is
|
||||||
|
* provided by the query
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* location
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder defaultLocation(GeoPoint point) {
|
||||||
|
this.defaultLocations.add(point.geohash());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the name of the field containing a geolocation to use
|
||||||
|
* @param fieldName name of the field
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public Builder field(String fieldName) {
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeolocationContextMapping build() {
|
||||||
|
if(precisions.isEmpty()) {
|
||||||
|
precisions.add(GeoHashUtils.PRECISION);
|
||||||
|
}
|
||||||
|
return new GeolocationContextMapping(name, precisions.toArray(), neighbors, defaultLocations, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GeoConfig extends ContextConfig {
|
||||||
|
|
||||||
|
private final GeolocationContextMapping mapping;
|
||||||
|
private final Collection<String> locations;
|
||||||
|
|
||||||
|
public GeoConfig(GeolocationContextMapping mapping, Collection<String> locations) {
|
||||||
|
this.locations = locations;
|
||||||
|
this.mapping = mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TokenStream wrapTokenStream(Document doc, TokenStream stream) {
|
||||||
|
Collection<String> geohashes;
|
||||||
|
|
||||||
|
if(locations == null | locations.size() == 0) {
|
||||||
|
if(mapping.fieldName != null) {
|
||||||
|
IndexableField[] fields = doc.getFields(mapping.fieldName);
|
||||||
|
if(fields.length > 0) {
|
||||||
|
geohashes = new ArrayList<String>(fields.length);
|
||||||
|
GeoPoint spare = new GeoPoint();
|
||||||
|
for (IndexableField field : fields) {
|
||||||
|
spare.resetFromString(field.stringValue());
|
||||||
|
geohashes.add(spare.geohash());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
geohashes = mapping.defaultLocations;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
geohashes = mapping.defaultLocations;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
geohashes = locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<String> locations = new HashSet<String>();
|
||||||
|
for (String geohash : geohashes) {
|
||||||
|
for (int p : mapping.precision) {
|
||||||
|
int precision = Math.min(p, geohash.length());
|
||||||
|
geohash = geohash.substring(0, precision);
|
||||||
|
if(mapping.neighbors) {
|
||||||
|
GeoHashUtils.addNeighbors(geohash, precision, locations);
|
||||||
|
}
|
||||||
|
locations.add(geohash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PrefixTokenFilter(stream, ContextMapping.SEPARATOR, locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder("GeoConfig(location = [");
|
||||||
|
Iterator<? extends CharSequence> location = this.locations.iterator();
|
||||||
|
if (location.hasNext()) {
|
||||||
|
sb.append(location.next());
|
||||||
|
while (location.hasNext()) {
|
||||||
|
sb.append(", ").append(location.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.append("])").toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GeoQuery extends ContextQuery {
|
||||||
|
private final String location;
|
||||||
|
private final int[] precisions;
|
||||||
|
|
||||||
|
public GeoQuery(String name, String location, int...precisions) {
|
||||||
|
super(name);
|
||||||
|
this.location = location;
|
||||||
|
this.precisions = precisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Automaton toAutomaton() {
|
||||||
|
Automaton automaton;
|
||||||
|
if(precisions == null || precisions.length == 0) {
|
||||||
|
automaton = BasicAutomata.makeString(location);
|
||||||
|
} else {
|
||||||
|
automaton = BasicAutomata.makeString(location.substring(0, Math.max(1, Math.min(location.length(), precisions[0]))));
|
||||||
|
for (int i = 1; i < precisions.length; i++) {
|
||||||
|
final String cell = location.substring(0, Math.max(1, Math.min(location.length(), precisions[0])));
|
||||||
|
automaton = BasicOperations.union(automaton, BasicAutomata.makeString(cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return automaton;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
if(precisions == null || precisions.length == 0) {
|
||||||
|
builder.field(name, location);
|
||||||
|
} else {
|
||||||
|
builder.startObject(name);
|
||||||
|
builder.field(FIELD_VALUE, location);
|
||||||
|
builder.field(FIELD_PRECISION, precisions);
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,7 +54,9 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
|
@ -468,8 +470,8 @@ public class GeoFilterTests extends ElasticsearchIntegrationTest {
|
||||||
String geohash = randomhash(10);
|
String geohash = randomhash(10);
|
||||||
logger.info("Testing geohash_cell filter for [{}]", geohash);
|
logger.info("Testing geohash_cell filter for [{}]", geohash);
|
||||||
|
|
||||||
List<String> neighbors = GeoHashUtils.neighbors(geohash);
|
Collection<? extends CharSequence> neighbors = GeoHashUtils.neighbors(geohash);
|
||||||
List<String> parentNeighbors = GeoHashUtils.neighbors(geohash.substring(0, geohash.length() - 1));
|
Collection<? extends CharSequence> parentNeighbors = GeoHashUtils.neighbors(geohash.substring(0, geohash.length() - 1));
|
||||||
|
|
||||||
logger.info("Neighbors {}", neighbors);
|
logger.info("Neighbors {}", neighbors);
|
||||||
logger.info("Parent Neighbors {}", parentNeighbors);
|
logger.info("Parent Neighbors {}", parentNeighbors);
|
||||||
|
@ -482,16 +484,18 @@ public class GeoFilterTests extends ElasticsearchIntegrationTest {
|
||||||
client().prepareIndex("locations", "location", "1").setCreate(true).setSource("pin", geohash).execute().actionGet();
|
client().prepareIndex("locations", "location", "1").setCreate(true).setSource("pin", geohash).execute().actionGet();
|
||||||
|
|
||||||
// index neighbors
|
// index neighbors
|
||||||
for (int i = 0; i < neighbors.size(); i++) {
|
Iterator<? extends CharSequence> iterator = neighbors.iterator();
|
||||||
client().prepareIndex("locations", "location", "N" + i).setCreate(true).setSource("pin", neighbors.get(i)).execute().actionGet();
|
for (int i = 0; iterator.hasNext(); i++) {
|
||||||
|
client().prepareIndex("locations", "location", "N" + i).setCreate(true).setSource("pin", iterator.next()).execute().actionGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index parent cell
|
// Index parent cell
|
||||||
client().prepareIndex("locations", "location", "p").setCreate(true).setSource("pin", geohash.substring(0, geohash.length() - 1)).execute().actionGet();
|
client().prepareIndex("locations", "location", "p").setCreate(true).setSource("pin", geohash.substring(0, geohash.length() - 1)).execute().actionGet();
|
||||||
|
|
||||||
// index neighbors
|
// index neighbors
|
||||||
for (int i = 0; i < parentNeighbors.size(); i++) {
|
iterator = parentNeighbors.iterator();
|
||||||
client().prepareIndex("locations", "location", "p" + i).setCreate(true).setSource("pin", parentNeighbors.get(i)).execute().actionGet();
|
for (int i = 0; iterator.hasNext(); i++) {
|
||||||
|
client().prepareIndex("locations", "location", "p" + i).setCreate(true).setSource("pin", iterator.next()).execute().actionGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
client().admin().indices().prepareRefresh("locations").execute().actionGet();
|
client().admin().indices().prepareRefresh("locations").execute().actionGet();
|
||||||
|
@ -531,24 +535,24 @@ public class GeoFilterTests extends ElasticsearchIntegrationTest {
|
||||||
@Test
|
@Test
|
||||||
public void testNeighbors() {
|
public void testNeighbors() {
|
||||||
// Simple root case
|
// Simple root case
|
||||||
assertThat(GeoHashUtils.neighbors("7"), containsInAnyOrder("4", "5", "6", "d", "e", "h", "k", "s"));
|
assertThat(GeoHashUtils.addNeighbors("7", new ArrayList<String>()), containsInAnyOrder("4", "5", "6", "d", "e", "h", "k", "s"));
|
||||||
|
|
||||||
// Root cases (Outer cells)
|
// Root cases (Outer cells)
|
||||||
assertThat(GeoHashUtils.neighbors("0"), containsInAnyOrder("1", "2", "3", "p", "r"));
|
assertThat(GeoHashUtils.addNeighbors("0", new ArrayList<String>()), containsInAnyOrder("1", "2", "3", "p", "r"));
|
||||||
assertThat(GeoHashUtils.neighbors("b"), containsInAnyOrder("8", "9", "c", "x", "z"));
|
assertThat(GeoHashUtils.addNeighbors("b", new ArrayList<String>()), containsInAnyOrder("8", "9", "c", "x", "z"));
|
||||||
assertThat(GeoHashUtils.neighbors("p"), containsInAnyOrder("n", "q", "r", "0", "2"));
|
assertThat(GeoHashUtils.addNeighbors("p", new ArrayList<String>()), containsInAnyOrder("n", "q", "r", "0", "2"));
|
||||||
assertThat(GeoHashUtils.neighbors("z"), containsInAnyOrder("8", "b", "w", "x", "y"));
|
assertThat(GeoHashUtils.addNeighbors("z", new ArrayList<String>()), containsInAnyOrder("8", "b", "w", "x", "y"));
|
||||||
|
|
||||||
// Root crossing dateline
|
// Root crossing dateline
|
||||||
assertThat(GeoHashUtils.neighbors("2"), containsInAnyOrder("0", "1", "3", "8", "9", "p", "r", "x"));
|
assertThat(GeoHashUtils.addNeighbors("2", new ArrayList<String>()), containsInAnyOrder("0", "1", "3", "8", "9", "p", "r", "x"));
|
||||||
assertThat(GeoHashUtils.neighbors("r"), containsInAnyOrder("0", "2", "8", "n", "p", "q", "w", "x"));
|
assertThat(GeoHashUtils.addNeighbors("r", new ArrayList<String>()), containsInAnyOrder("0", "2", "8", "n", "p", "q", "w", "x"));
|
||||||
|
|
||||||
// level1: simple case
|
// level1: simple case
|
||||||
assertThat(GeoHashUtils.neighbors("dk"), containsInAnyOrder("d5", "d7", "de", "dh", "dj", "dm", "ds", "dt"));
|
assertThat(GeoHashUtils.addNeighbors("dk", new ArrayList<String>()), containsInAnyOrder("d5", "d7", "de", "dh", "dj", "dm", "ds", "dt"));
|
||||||
|
|
||||||
// Level1: crossing cells
|
// Level1: crossing cells
|
||||||
assertThat(GeoHashUtils.neighbors("d5"), containsInAnyOrder("d4", "d6", "d7", "dh", "dk", "9f", "9g", "9u"));
|
assertThat(GeoHashUtils.addNeighbors("d5", new ArrayList<String>()), containsInAnyOrder("d4", "d6", "d7", "dh", "dk", "9f", "9g", "9u"));
|
||||||
assertThat(GeoHashUtils.neighbors("d0"), containsInAnyOrder("d1", "d2", "d3", "9b", "9c", "6p", "6r", "3z"));
|
assertThat(GeoHashUtils.addNeighbors("d0", new ArrayList<String>()), containsInAnyOrder("d1", "d2", "d3", "9b", "9c", "6p", "6r", "3z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double distance(double lat1, double lon1, double lat2, double lon2) {
|
public static double distance(double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
|
|
@ -0,0 +1,640 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch 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.search.suggest;
|
||||||
|
|
||||||
|
import com.carrotsearch.randomizedtesting.generators.RandomStrings;
|
||||||
|
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
|
||||||
|
import org.elasticsearch.action.suggest.SuggestRequestBuilder;
|
||||||
|
import org.elasticsearch.action.suggest.SuggestResponse;
|
||||||
|
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.unit.Fuzziness;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.search.suggest.Suggest.Suggestion;
|
||||||
|
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry;
|
||||||
|
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option;
|
||||||
|
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
|
||||||
|
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
|
||||||
|
import org.elasticsearch.search.suggest.completion.CompletionSuggestionFuzzyBuilder;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextBuilder;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping;
|
||||||
|
import org.elasticsearch.test.ElasticsearchIntegrationTest;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
|
||||||
|
import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS;
|
||||||
|
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
|
||||||
|
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchGeoAssertions.assertDistance;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
|
public class ContextSuggestSearchTests extends ElasticsearchIntegrationTest {
|
||||||
|
|
||||||
|
private static final String INDEX = "test";
|
||||||
|
private static final String TYPE = "testType";
|
||||||
|
private static final String FIELD = "testField";
|
||||||
|
|
||||||
|
private static final String[][] HEROS = {
|
||||||
|
{ "Afari, Jamal", "Jamal Afari", "Jamal" },
|
||||||
|
{ "Allerdyce, St. John", "Allerdyce, John", "St. John", "St. John Allerdyce" },
|
||||||
|
{ "Beaubier, Jean-Paul", "Jean-Paul Beaubier", "Jean-Paul" },
|
||||||
|
{ "Beaubier, Jeanne-Marie", "Jeanne-Marie Beaubier", "Jeanne-Marie" },
|
||||||
|
{ "Braddock, Elizabeth \"Betsy\"", "Betsy", "Braddock, Elizabeth", "Elizabeth Braddock", "Elizabeth" },
|
||||||
|
{ "Cody Mushumanski gun Man", "the hunter", "gun man", "Cody Mushumanski" },
|
||||||
|
{ "Corbo, Adrian", "Adrian Corbo", "Adrian" },
|
||||||
|
{ "Corbo, Jared", "Jared Corbo", "Jared" },
|
||||||
|
{ "Creel, Carl \"Crusher\"", "Creel, Carl", "Crusher", "Carl Creel", "Carl" },
|
||||||
|
{ "Crichton, Lady Jacqueline Falsworth", "Lady Jacqueline Falsworth Crichton", "Lady Jacqueline Falsworth",
|
||||||
|
"Jacqueline Falsworth" }, { "Crichton, Kenneth", "Kenneth Crichton", "Kenneth" },
|
||||||
|
{ "MacKenzie, Al", "Al MacKenzie", "Al" },
|
||||||
|
{ "MacPherran, Mary \"Skeeter\"", "Mary MacPherran \"Skeeter\"", "MacPherran, Mary", "Skeeter", "Mary MacPherran" },
|
||||||
|
{ "MacTaggert, Moira", "Moira MacTaggert", "Moira" }, { "Rasputin, Illyana", "Illyana Rasputin", "Illyana" },
|
||||||
|
{ "Rasputin, Mikhail", "Mikhail Rasputin", "Mikhail" }, { "Rasputin, Piotr", "Piotr Rasputin", "Piotr" },
|
||||||
|
{ "Smythe, Alistair", "Alistair Smythe", "Alistair" }, { "Smythe, Spencer", "Spencer Smythe", "Spencer" },
|
||||||
|
{ "Whitemane, Aelfyre", "Aelfyre Whitemane", "Aelfyre" }, { "Whitemane, Kofi", "Kofi Whitemane", "Kofi" } };
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasicGeo() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.location("st").precision("5km").neighbors(true));
|
||||||
|
XContentBuilder source1 = jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startObject(FIELD)
|
||||||
|
.array("input", "Hotel Amsterdam", "Amsterdam")
|
||||||
|
.field("output", "Hotel Amsterdam in Berlin")
|
||||||
|
.startObject("context").latlon("st", 52.529172, 13.407333).endObject()
|
||||||
|
.endObject()
|
||||||
|
.endObject();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "1").setSource(source1).execute().actionGet();
|
||||||
|
|
||||||
|
XContentBuilder source2 = jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startObject(FIELD)
|
||||||
|
.array("input", "Hotel Berlin", "Berlin")
|
||||||
|
.field("output", "Hotel Berlin in Amsterdam")
|
||||||
|
.startObject("context").latlon("st", 52.363389, 4.888695).endObject()
|
||||||
|
.endObject()
|
||||||
|
.endObject();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "2").setSource(source2).execute().actionGet();
|
||||||
|
|
||||||
|
client().admin().indices().prepareRefresh(INDEX).get();
|
||||||
|
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text("h").size(10)
|
||||||
|
.addGeoLocation("st", 52.52, 13.4);
|
||||||
|
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
|
||||||
|
assertEquals(suggestResponse.getSuggest().size(), 1);
|
||||||
|
assertEquals("Hotel Amsterdam in Berlin", suggestResponse.getSuggest().getSuggestion(suggestionName).iterator().next().getOptions().iterator().next().getText().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGeoField() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
|
||||||
|
XContentBuilder mapping = jsonBuilder();
|
||||||
|
mapping.startObject();
|
||||||
|
mapping.startObject(TYPE);
|
||||||
|
mapping.startObject("properties");
|
||||||
|
mapping.startObject("pin");
|
||||||
|
mapping.field("type", "geo_point");
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.startObject(FIELD);
|
||||||
|
mapping.field("type", "completion");
|
||||||
|
mapping.field("index_analyzer", "simple");
|
||||||
|
mapping.field("search_analyzer", "simple");
|
||||||
|
|
||||||
|
mapping.startObject("context");
|
||||||
|
mapping.value(ContextBuilder.location("st", 5, true).field("pin").build());
|
||||||
|
mapping.endObject();
|
||||||
|
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.endObject();
|
||||||
|
|
||||||
|
PutMappingResponse putMappingResponse = client().admin().indices().preparePutMapping(INDEX).setType(TYPE).setSource(mapping).get();
|
||||||
|
|
||||||
|
assertThat(putMappingResponse.isAcknowledged(), is(true));
|
||||||
|
ensureYellow();
|
||||||
|
|
||||||
|
XContentBuilder source1 = jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.latlon("pin", 52.529172, 13.407333)
|
||||||
|
.startObject(FIELD)
|
||||||
|
.array("input", "Hotel Amsterdam", "Amsterdam")
|
||||||
|
.field("output", "Hotel Amsterdam in Berlin")
|
||||||
|
.startObject("context").endObject()
|
||||||
|
.endObject()
|
||||||
|
.endObject();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "1").setSource(source1).execute().actionGet();
|
||||||
|
|
||||||
|
XContentBuilder source2 = jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.latlon("pin", 52.363389, 4.888695)
|
||||||
|
.startObject(FIELD)
|
||||||
|
.array("input", "Hotel Berlin", "Berlin")
|
||||||
|
.field("output", "Hotel Berlin in Amsterdam")
|
||||||
|
.startObject("context").endObject()
|
||||||
|
.endObject()
|
||||||
|
.endObject();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "2").setSource(source2).execute().actionGet();
|
||||||
|
|
||||||
|
client().admin().indices().prepareRefresh(INDEX).get();
|
||||||
|
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text("h").size(10)
|
||||||
|
.addGeoLocation("st", 52.52, 13.4);
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
|
||||||
|
assertEquals(suggestResponse.getSuggest().size(), 1);
|
||||||
|
assertEquals("Hotel Amsterdam in Berlin", suggestResponse.getSuggest().getSuggestion(suggestionName).iterator().next().getOptions().iterator().next().getText().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimpleGeo() throws Exception {
|
||||||
|
String reinickendorf = "u337p3mp11e2";
|
||||||
|
String pankow = "u33e0cyyjur4";
|
||||||
|
String koepenick = "u33dm4f7fn40";
|
||||||
|
String bernau = "u33etnjf1yjn";
|
||||||
|
String berlin = "u33dc1v0xupz";
|
||||||
|
String mitte = "u33dc0cpke4q";
|
||||||
|
String steglitz = "u336m36rjh2p";
|
||||||
|
String wilmersdorf = "u336wmw0q41s";
|
||||||
|
String spandau = "u336uqek7gh6";
|
||||||
|
String tempelhof = "u33d91jh3by0";
|
||||||
|
String schoeneberg = "u336xdrkzbq7";
|
||||||
|
String treptow = "u33d9unn7fp7";
|
||||||
|
|
||||||
|
double precision = 100.0; // meters
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.location("st").precision(precision).neighbors(true));
|
||||||
|
|
||||||
|
String[] locations = { reinickendorf, pankow, koepenick, bernau, berlin, mitte, steglitz, wilmersdorf, spandau, tempelhof,
|
||||||
|
schoeneberg, treptow };
|
||||||
|
|
||||||
|
String[][] input = { { "pizza - reinickendorf", "pizza", "food" }, { "pizza - pankow", "pizza", "food" },
|
||||||
|
{ "pizza - koepenick", "pizza", "food" }, { "pizza - bernau", "pizza", "food" }, { "pizza - berlin", "pizza", "food" },
|
||||||
|
{ "pizza - mitte", "pizza - berlin mitte", "pizza", "food" },
|
||||||
|
{ "pizza - steglitz", "pizza - Berlin-Steglitz", "pizza", "food" }, { "pizza - wilmersdorf", "pizza", "food" },
|
||||||
|
{ "pizza - spandau", "spandau bei berlin", "pizza", "food" },
|
||||||
|
{ "pizza - tempelhof", "pizza - berlin-tempelhof", "pizza", "food" },
|
||||||
|
{ "pizza - schoeneberg", "pizza - schöneberg", "pizza - berlin schoeneberg", "pizza", "food" },
|
||||||
|
{ "pizza - treptow", "pizza", "food" } };
|
||||||
|
|
||||||
|
for (int i = 0; i < locations.length; i++) {
|
||||||
|
XContentBuilder source = jsonBuilder().startObject().startObject(FIELD).startArray("input").value(input[i]).endArray()
|
||||||
|
.startObject("context").field("st", locations[i]).endObject().field("payload", locations[i]).endObject().endObject();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "" + i).setSource(source).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertGeoSuggestionsInRange(berlin, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(reinickendorf, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(spandau, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(koepenick, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(schoeneberg, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(tempelhof, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(bernau, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(pankow, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(mitte, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(steglitz, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(mitte, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(wilmersdorf, "pizza", precision);
|
||||||
|
assertGeoSuggestionsInRange(treptow, "pizza", precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimplePrefix() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.category("st"));
|
||||||
|
|
||||||
|
for (int i = 0; i < HEROS.length; i++) {
|
||||||
|
XContentBuilder source = jsonBuilder().startObject().startObject(FIELD).startArray("input").value(HEROS[i]).endArray()
|
||||||
|
.startObject("context").field("st", i%3).endObject()
|
||||||
|
.startObject("payload").field("group", i % 3).field("id", i).endObject()
|
||||||
|
.endObject().endObject();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "" + i).setSource(source).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertPrefixSuggestions(0, "a", "Afari, Jamal", "Adrian Corbo", "Adrian");
|
||||||
|
assertPrefixSuggestions(0, "b", "Beaubier, Jeanne-Marie");
|
||||||
|
assertPrefixSuggestions(0, "c", "Corbo, Adrian", "Crichton, Lady Jacqueline Falsworth");
|
||||||
|
assertPrefixSuggestions(0, "mary", "Mary MacPherran \"Skeeter\"", "Mary MacPherran");
|
||||||
|
assertPrefixSuggestions(0, "s", "Skeeter", "Smythe, Spencer", "Spencer Smythe", "Spencer");
|
||||||
|
assertPrefixSuggestions(1, "s", "St. John", "St. John Allerdyce");
|
||||||
|
assertPrefixSuggestions(2, "s", "Smythe, Alistair");
|
||||||
|
assertPrefixSuggestions(1, "w", "Whitemane, Aelfyre");
|
||||||
|
assertPrefixSuggestions(2, "w", "Whitemane, Kofi");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasic() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, false, ContextBuilder.reference("st", "_type"), ContextBuilder.reference("nd", "_type"));
|
||||||
|
|
||||||
|
client().prepareIndex(INDEX, TYPE, "1")
|
||||||
|
.setSource(
|
||||||
|
jsonBuilder().startObject().startObject(FIELD).startArray("input").value("my hotel").value("this hotel").endArray()
|
||||||
|
.startObject("context").endObject()
|
||||||
|
.field("payload", TYPE + "|" + TYPE).endObject().endObject()).execute()
|
||||||
|
.actionGet();
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertDoubleFieldSuggestions(TYPE, TYPE, "m", "my hotel");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimpleField() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.reference("st", "category"));
|
||||||
|
|
||||||
|
for (int i = 0; i < HEROS.length; i++) {
|
||||||
|
client().prepareIndex(INDEX, TYPE, "" + i)
|
||||||
|
.setSource(
|
||||||
|
jsonBuilder().startObject().field("category", Integer.toString(i % 3)).startObject(FIELD).startArray("input")
|
||||||
|
.value(HEROS[i]).endArray().startObject("context").endObject().field("payload", Integer.toString(i % 3))
|
||||||
|
.endObject().endObject()).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertFieldSuggestions("0", "a", "Afari, Jamal", "Adrian Corbo", "Adrian");
|
||||||
|
assertFieldSuggestions("0", "b", "Beaubier, Jeanne-Marie");
|
||||||
|
assertFieldSuggestions("0", "c", "Corbo, Adrian", "Crichton, Lady Jacqueline Falsworth");
|
||||||
|
assertFieldSuggestions("0", "mary", "Mary MacPherran \"Skeeter\"", "Mary MacPherran");
|
||||||
|
assertFieldSuggestions("0", "s", "Skeeter", "Smythe, Spencer", "Spencer Smythe", "Spencer");
|
||||||
|
assertFieldSuggestions("1", "s", "St. John", "St. John Allerdyce");
|
||||||
|
assertFieldSuggestions("2", "s", "Smythe, Alistair");
|
||||||
|
assertFieldSuggestions("1", "w", "Whitemane, Aelfyre");
|
||||||
|
assertFieldSuggestions("2", "w", "Whitemane, Kofi");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiValueField() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.reference("st", "category"));
|
||||||
|
|
||||||
|
for (int i = 0; i < HEROS.length; i++) {
|
||||||
|
client().prepareIndex(INDEX, TYPE, "" + i)
|
||||||
|
.setSource(
|
||||||
|
jsonBuilder().startObject().startArray("category").value(Integer.toString(i % 3)).value("other").endArray()
|
||||||
|
.startObject(FIELD).startArray("input").value(HEROS[i]).endArray().startObject("context").endObject()
|
||||||
|
.field("payload", Integer.toString(i % 3)).endObject().endObject()).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertFieldSuggestions("0", "a", "Afari, Jamal", "Adrian Corbo", "Adrian");
|
||||||
|
assertFieldSuggestions("0", "b", "Beaubier, Jeanne-Marie");
|
||||||
|
assertFieldSuggestions("0", "c", "Corbo, Adrian", "Crichton, Lady Jacqueline Falsworth");
|
||||||
|
assertFieldSuggestions("0", "mary", "Mary MacPherran \"Skeeter\"", "Mary MacPherran");
|
||||||
|
assertFieldSuggestions("0", "s", "Skeeter", "Smythe, Spencer", "Spencer Smythe", "Spencer");
|
||||||
|
assertFieldSuggestions("1", "s", "St. John", "St. John Allerdyce");
|
||||||
|
assertFieldSuggestions("2", "s", "Smythe, Alistair");
|
||||||
|
assertFieldSuggestions("1", "w", "Whitemane, Aelfyre");
|
||||||
|
assertFieldSuggestions("2", "w", "Whitemane, Kofi");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiContext() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.reference("st", "categoryA"), ContextBuilder.reference("nd", "categoryB"));
|
||||||
|
|
||||||
|
for (int i = 0; i < HEROS.length; i++) {
|
||||||
|
client().prepareIndex(INDEX, TYPE, "" + i)
|
||||||
|
.setSource(
|
||||||
|
jsonBuilder().startObject().field("categoryA").value("" + (char) ('0' + (i % 3))).field("categoryB")
|
||||||
|
.value("" + (char) ('A' + (i % 3))).startObject(FIELD).startArray("input").value(HEROS[i]).endArray()
|
||||||
|
.startObject("context").endObject().field("payload", ((char) ('0' + (i % 3))) + "" + (char) ('A' + (i % 3)))
|
||||||
|
.endObject().endObject()).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertMultiContextSuggestions("0", "A", "a", "Afari, Jamal", "Adrian Corbo", "Adrian");
|
||||||
|
assertMultiContextSuggestions("0", "A", "b", "Beaubier, Jeanne-Marie");
|
||||||
|
assertMultiContextSuggestions("0", "A", "c", "Corbo, Adrian", "Crichton, Lady Jacqueline Falsworth");
|
||||||
|
assertMultiContextSuggestions("0", "A", "mary", "Mary MacPherran \"Skeeter\"", "Mary MacPherran");
|
||||||
|
assertMultiContextSuggestions("0", "A", "s", "Skeeter", "Smythe, Spencer", "Spencer Smythe", "Spencer");
|
||||||
|
assertMultiContextSuggestions("1", "B", "s", "St. John", "St. John Allerdyce");
|
||||||
|
assertMultiContextSuggestions("2", "C", "s", "Smythe, Alistair");
|
||||||
|
assertMultiContextSuggestions("1", "B", "w", "Whitemane, Aelfyre");
|
||||||
|
assertMultiContextSuggestions("2", "C", "w", "Whitemane, Kofi");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiContextWithFuzzyLogic() throws Exception {
|
||||||
|
createIndexAndSettings();
|
||||||
|
createMapping(TYPE, ContextBuilder.reference("st", "categoryA"), ContextBuilder.reference("nd", "categoryB"));
|
||||||
|
|
||||||
|
for (int i = 0; i < HEROS.length; i++) {
|
||||||
|
String source = jsonBuilder().startObject().field("categoryA", "" + (char) ('0' + (i % 3)))
|
||||||
|
.field("categoryB", "" + (char) ('a' + (i % 3))).startObject(FIELD).array("input", HEROS[i])
|
||||||
|
.startObject("context").endObject().startObject("payload").field("categoryA", "" + (char) ('0' + (i % 3)))
|
||||||
|
.field("categoryB", "" + (char) ('a' + (i % 3))).endObject().endObject().endObject().string();
|
||||||
|
client().prepareIndex(INDEX, TYPE, "" + i).setSource(source).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
String[] prefix1 = { "0", "1", "2" };
|
||||||
|
String[] prefix2 = { "a", "b", "c" };
|
||||||
|
String[] prefix3 = { "0", "1" };
|
||||||
|
String[] prefix4 = { "a", "b" };
|
||||||
|
|
||||||
|
assertContextWithFuzzySuggestions(prefix1, prefix2, "mary", "MacKenzie, Al", "MacPherran, Mary", "MacPherran, Mary \"Skeeter\"",
|
||||||
|
"MacTaggert, Moira", "Mary MacPherran", "Mary MacPherran \"Skeeter\"");
|
||||||
|
assertContextWithFuzzySuggestions(prefix1, prefix2, "mac", "Mikhail", "Mary MacPherran \"Skeeter\"", "MacTaggert, Moira",
|
||||||
|
"Moira MacTaggert", "Moira", "MacKenzie, Al", "Mary MacPherran", "Mikhail Rasputin", "MacPherran, Mary",
|
||||||
|
"MacPherran, Mary \"Skeeter\"");
|
||||||
|
assertContextWithFuzzySuggestions(prefix3, prefix4, "mary", "MacPherran, Mary", "MacPherran, Mary \"Skeeter\"",
|
||||||
|
"MacTaggert, Moira", "Mary MacPherran", "Mary MacPherran \"Skeeter\"");
|
||||||
|
assertContextWithFuzzySuggestions(prefix3, prefix4, "mac", "MacPherran, Mary", "MacPherran, Mary \"Skeeter\"", "MacTaggert, Moira",
|
||||||
|
"Mary MacPherran", "Mary MacPherran \"Skeeter\"", "Mikhail", "Mikhail Rasputin", "Moira", "Moira MacTaggert");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimpleType() throws Exception {
|
||||||
|
String[] types = { TYPE + "A", TYPE + "B", TYPE + "C" };
|
||||||
|
|
||||||
|
createIndexAndSettings();
|
||||||
|
for (int i = 0; i < types.length; i++) {
|
||||||
|
createMapping(types[i], ContextBuilder.reference("st", "_type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < HEROS.length; i++) {
|
||||||
|
String type = types[i % types.length];
|
||||||
|
client().prepareIndex(INDEX, type, "" + i)
|
||||||
|
.setSource(
|
||||||
|
jsonBuilder().startObject().startObject(FIELD).startArray("input").value(HEROS[i]).endArray()
|
||||||
|
.startObject("context").endObject().field("payload", type).endObject().endObject()).execute().actionGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
assertFieldSuggestions(types[0], "a", "Afari, Jamal", "Adrian Corbo", "Adrian");
|
||||||
|
assertFieldSuggestions(types[0], "b", "Beaubier, Jeanne-Marie");
|
||||||
|
assertFieldSuggestions(types[0], "c", "Corbo, Adrian", "Crichton, Lady Jacqueline Falsworth");
|
||||||
|
assertFieldSuggestions(types[0], "mary", "Mary MacPherran \"Skeeter\"", "Mary MacPherran");
|
||||||
|
assertFieldSuggestions(types[0], "s", "Skeeter", "Smythe, Spencer", "Spencer Smythe", "Spencer");
|
||||||
|
assertFieldSuggestions(types[1], "s", "St. John", "St. John Allerdyce");
|
||||||
|
assertFieldSuggestions(types[2], "s", "Smythe, Alistair");
|
||||||
|
assertFieldSuggestions(types[1], "w", "Whitemane, Aelfyre");
|
||||||
|
assertFieldSuggestions(types[2], "w", "Whitemane, Kofi");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertGeoSuggestionsInRange(String location, String suggest, double precision) throws IOException {
|
||||||
|
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text(suggest).size(10)
|
||||||
|
.addGeoLocation("st", location);
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
|
||||||
|
Suggest suggest2 = suggestResponse.getSuggest();
|
||||||
|
assertTrue(suggest2.iterator().hasNext());
|
||||||
|
for (Suggestion<? extends Entry<? extends Option>> s : suggest2) {
|
||||||
|
CompletionSuggestion suggestion = (CompletionSuggestion) s;
|
||||||
|
assertTrue(suggestion.iterator().hasNext());
|
||||||
|
for (CompletionSuggestion.Entry entry : suggestion) {
|
||||||
|
List<CompletionSuggestion.Entry.Option> options = entry.getOptions();
|
||||||
|
assertTrue(options.iterator().hasNext());
|
||||||
|
for (CompletionSuggestion.Entry.Option option : options) {
|
||||||
|
String target = option.getPayloadAsString();
|
||||||
|
assertDistance(location, target, Matchers.lessThanOrEqualTo(precision));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertPrefixSuggestions(long prefix, String suggest, String... hits) throws IOException {
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text(suggest)
|
||||||
|
.size(hits.length + 1).addCategory("st", Long.toString(prefix));
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
ArrayList<String> suggestions = new ArrayList<String>();
|
||||||
|
Suggest suggest2 = suggestResponse.getSuggest();
|
||||||
|
assertTrue(suggest2.iterator().hasNext());
|
||||||
|
for (Suggestion<? extends Entry<? extends Option>> s : suggest2) {
|
||||||
|
CompletionSuggestion suggestion = (CompletionSuggestion) s;
|
||||||
|
for (CompletionSuggestion.Entry entry : suggestion) {
|
||||||
|
List<CompletionSuggestion.Entry.Option> options = entry.getOptions();
|
||||||
|
for (CompletionSuggestion.Entry.Option option : options) {
|
||||||
|
Map<String, Object> payload = option.getPayloadAsMap();
|
||||||
|
int group = (Integer) payload.get("group");
|
||||||
|
String text = option.getText().string();
|
||||||
|
assertEquals(prefix, group);
|
||||||
|
suggestions.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertSuggestionsMatch(suggestions, hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertContextWithFuzzySuggestions(String[] prefix1, String[] prefix2, String suggest, String... hits) throws IOException {
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionFuzzyBuilder context = new CompletionSuggestionFuzzyBuilder(suggestionName).field(FIELD).text(suggest)
|
||||||
|
.size(hits.length + 10).addContextField("st", prefix1).addContextField("nd", prefix2).setFuzziness(Fuzziness.TWO);
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
|
||||||
|
ArrayList<String> suggestions = new ArrayList<String>();
|
||||||
|
|
||||||
|
Suggest suggest2 = suggestResponse.getSuggest();
|
||||||
|
assertTrue(suggest2.iterator().hasNext());
|
||||||
|
for (Suggestion<? extends Entry<? extends Option>> s : suggest2) {
|
||||||
|
CompletionSuggestion suggestion = (CompletionSuggestion) s;
|
||||||
|
for (CompletionSuggestion.Entry entry : suggestion) {
|
||||||
|
List<CompletionSuggestion.Entry.Option> options = entry.getOptions();
|
||||||
|
for (CompletionSuggestion.Entry.Option option : options) {
|
||||||
|
Map<String, Object> payload = option.getPayloadAsMap();
|
||||||
|
String text = option.getText().string();
|
||||||
|
assertThat(prefix1, Matchers.hasItemInArray(payload.get("categoryA")));
|
||||||
|
assertThat(prefix2, Matchers.hasItemInArray(payload.get("categoryB")));
|
||||||
|
suggestions.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSuggestionsMatch(suggestions, hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertFieldSuggestions(String value, String suggest, String... hits) throws IOException {
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text(suggest).size(10)
|
||||||
|
.addContextField("st", value);
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
|
||||||
|
ArrayList<String> suggestions = new ArrayList<String>();
|
||||||
|
|
||||||
|
Suggest suggest2 = suggestResponse.getSuggest();
|
||||||
|
for (Suggestion<? extends Entry<? extends Option>> s : suggest2) {
|
||||||
|
CompletionSuggestion suggestion = (CompletionSuggestion) s;
|
||||||
|
for (CompletionSuggestion.Entry entry : suggestion) {
|
||||||
|
List<CompletionSuggestion.Entry.Option> options = entry.getOptions();
|
||||||
|
for (CompletionSuggestion.Entry.Option option : options) {
|
||||||
|
String payload = option.getPayloadAsString();
|
||||||
|
String text = option.getText().string();
|
||||||
|
assertEquals(value, payload);
|
||||||
|
suggestions.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertSuggestionsMatch(suggestions, hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertDoubleFieldSuggestions(String field1, String field2, String suggest, String... hits) throws IOException {
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text(suggest).size(10)
|
||||||
|
.addContextField("st", field1).addContextField("nd", field2);
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
ArrayList<String> suggestions = new ArrayList<String>();
|
||||||
|
|
||||||
|
Suggest suggest2 = suggestResponse.getSuggest();
|
||||||
|
for (Suggestion<? extends Entry<? extends Option>> s : suggest2) {
|
||||||
|
CompletionSuggestion suggestion = (CompletionSuggestion) s;
|
||||||
|
for (CompletionSuggestion.Entry entry : suggestion) {
|
||||||
|
List<CompletionSuggestion.Entry.Option> options = entry.getOptions();
|
||||||
|
for (CompletionSuggestion.Entry.Option option : options) {
|
||||||
|
String payload = option.getPayloadAsString();
|
||||||
|
String text = option.getText().string();
|
||||||
|
assertEquals(field1 + "|" + field2, payload);
|
||||||
|
suggestions.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertSuggestionsMatch(suggestions, hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertMultiContextSuggestions(String value1, String value2, String suggest, String... hits) throws IOException {
|
||||||
|
String suggestionName = RandomStrings.randomAsciiOfLength(new Random(), 10);
|
||||||
|
CompletionSuggestionBuilder context = new CompletionSuggestionBuilder(suggestionName).field(FIELD).text(suggest).size(10)
|
||||||
|
.addContextField("st", value1).addContextField("nd", value2);
|
||||||
|
|
||||||
|
SuggestRequestBuilder suggestionRequest = client().prepareSuggest(INDEX).addSuggestion(context);
|
||||||
|
SuggestResponse suggestResponse = suggestionRequest.execute().actionGet();
|
||||||
|
ArrayList<String> suggestions = new ArrayList<String>();
|
||||||
|
|
||||||
|
Suggest suggest2 = suggestResponse.getSuggest();
|
||||||
|
for (Suggestion<? extends Entry<? extends Option>> s : suggest2) {
|
||||||
|
CompletionSuggestion suggestion = (CompletionSuggestion) s;
|
||||||
|
for (CompletionSuggestion.Entry entry : suggestion) {
|
||||||
|
List<CompletionSuggestion.Entry.Option> options = entry.getOptions();
|
||||||
|
for (CompletionSuggestion.Entry.Option option : options) {
|
||||||
|
String payload = option.getPayloadAsString();
|
||||||
|
String text = option.getText().string();
|
||||||
|
assertEquals(value1 + value2, payload);
|
||||||
|
suggestions.add(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertSuggestionsMatch(suggestions, hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSuggestionsMatch(List<String> suggestions, String... hits) {
|
||||||
|
boolean[] suggested = new boolean[hits.length];
|
||||||
|
Arrays.sort(hits);
|
||||||
|
Arrays.fill(suggested, false);
|
||||||
|
int numSuggestions = 0;
|
||||||
|
|
||||||
|
for (String suggestion : suggestions) {
|
||||||
|
int hitpos = Arrays.binarySearch(hits, suggestion);
|
||||||
|
|
||||||
|
assertEquals(hits[hitpos], suggestion);
|
||||||
|
assertTrue(hitpos >= 0);
|
||||||
|
assertTrue(!suggested[hitpos]);
|
||||||
|
|
||||||
|
suggested[hitpos] = true;
|
||||||
|
numSuggestions++;
|
||||||
|
|
||||||
|
}
|
||||||
|
assertEquals(hits.length, numSuggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createMapping(String type, ContextBuilder<?>... context) throws IOException {
|
||||||
|
createMapping(type, false, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createMapping(String type, boolean preserveSeparators, ContextBuilder<?>... context) throws IOException {
|
||||||
|
createMapping(type, "simple", "simple", true, preserveSeparators, true, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImmutableSettings.Builder createDefaultSettings() {
|
||||||
|
int randomShardNumber = between(1, 5);
|
||||||
|
int randomReplicaNumber = between(0, cluster().size() - 1);
|
||||||
|
return settingsBuilder().put(SETTING_NUMBER_OF_SHARDS, randomShardNumber).put(SETTING_NUMBER_OF_REPLICAS, randomReplicaNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createIndexAndSettings() throws IOException {
|
||||||
|
createIndexAndSettings(createDefaultSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createIndexAndSettings(Settings.Builder settingsBuilder) throws IOException {
|
||||||
|
client().admin().indices().prepareCreate(INDEX).setSettings(settingsBuilder).get();
|
||||||
|
ensureYellow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createMapping(String type, String indexAnalyzer, String searchAnalyzer, boolean payloads, boolean preserveSeparators,
|
||||||
|
boolean preservePositionIncrements, ContextBuilder<?>... contexts) throws IOException {
|
||||||
|
XContentBuilder mapping = jsonBuilder();
|
||||||
|
mapping.startObject();
|
||||||
|
mapping.startObject(type);
|
||||||
|
mapping.startObject("properties");
|
||||||
|
mapping.startObject(FIELD);
|
||||||
|
mapping.field("type", "completion");
|
||||||
|
mapping.field("index_analyzer", indexAnalyzer);
|
||||||
|
mapping.field("search_analyzer", searchAnalyzer);
|
||||||
|
mapping.field("payloads", payloads);
|
||||||
|
mapping.field("preserve_separators", preserveSeparators);
|
||||||
|
mapping.field("preserve_position_increments", preservePositionIncrements);
|
||||||
|
|
||||||
|
mapping.startObject("context");
|
||||||
|
for (ContextBuilder<? extends ContextMapping> context : contexts) {
|
||||||
|
mapping.value(context.build());
|
||||||
|
}
|
||||||
|
mapping.endObject();
|
||||||
|
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.endObject();
|
||||||
|
mapping.endObject();
|
||||||
|
|
||||||
|
PutMappingResponse putMappingResponse = client().admin().indices().preparePutMapping(INDEX).setType(type).setSource(mapping).get();
|
||||||
|
|
||||||
|
assertThat(putMappingResponse.isAcknowledged(), is(true));
|
||||||
|
ensureYellow();
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,16 +31,15 @@ import org.apache.lucene.store.IndexOutput;
|
||||||
import org.apache.lucene.util.BytesRef;
|
import org.apache.lucene.util.BytesRef;
|
||||||
import org.apache.lucene.util.IOUtils;
|
import org.apache.lucene.util.IOUtils;
|
||||||
import org.apache.lucene.util.IntsRef;
|
import org.apache.lucene.util.IntsRef;
|
||||||
import org.apache.lucene.util.fst.ByteSequenceOutputs;
|
import org.apache.lucene.util.automaton.Automaton;
|
||||||
import org.apache.lucene.util.fst.FST;
|
import org.apache.lucene.util.fst.*;
|
||||||
import org.apache.lucene.util.fst.PairOutputs;
|
|
||||||
import org.apache.lucene.util.fst.PairOutputs.Pair;
|
import org.apache.lucene.util.fst.PairOutputs.Pair;
|
||||||
import org.apache.lucene.util.fst.PositiveIntOutputs;
|
|
||||||
import org.elasticsearch.common.regex.Regex;
|
import org.elasticsearch.common.regex.Regex;
|
||||||
import org.elasticsearch.index.mapper.FieldMapper;
|
import org.elasticsearch.index.mapper.core.CompletionFieldMapper;
|
||||||
|
import org.elasticsearch.search.suggest.completion.AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder;
|
||||||
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.CompletionLookupProvider;
|
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.CompletionLookupProvider;
|
||||||
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.LookupFactory;
|
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.LookupFactory;
|
||||||
import org.elasticsearch.search.suggest.completion.AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder;
|
import org.elasticsearch.search.suggest.context.ContextMapping.ContextQuery;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -84,7 +83,7 @@ public class AnalyzingCompletionLookupProviderV1 extends CompletionLookupProvide
|
||||||
int options = preserveSep ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
int options = preserveSep ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
||||||
// needs to fixed in the suggester first before it can be supported
|
// needs to fixed in the suggester first before it can be supported
|
||||||
//options |= exactFirst ? XAnalyzingSuggester.EXACT_FIRST : 0;
|
//options |= exactFirst ? XAnalyzingSuggester.EXACT_FIRST : 0;
|
||||||
prototype = new XAnalyzingSuggester(null, null, options, maxSurfaceFormsPerAnalyzedForm, maxGraphExpansions, preservePositionIncrements,
|
prototype = new XAnalyzingSuggester(null,null, null, options, maxSurfaceFormsPerAnalyzedForm, maxGraphExpansions, preservePositionIncrements,
|
||||||
null, false, 1, SEP_LABEL, PAYLOAD_SEP, END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
null, false, 1, SEP_LABEL, PAYLOAD_SEP, END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,24 +242,25 @@ public class AnalyzingCompletionLookupProviderV1 extends CompletionLookupProvide
|
||||||
final long ramBytesUsed = sizeInBytes;
|
final long ramBytesUsed = sizeInBytes;
|
||||||
return new LookupFactory() {
|
return new LookupFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Lookup getLookup(FieldMapper<?> mapper, CompletionSuggestionContext suggestionContext) {
|
public Lookup getLookup(CompletionFieldMapper mapper, CompletionSuggestionContext suggestionContext) {
|
||||||
AnalyzingSuggestHolder analyzingSuggestHolder = lookupMap.get(mapper.names().indexName());
|
AnalyzingSuggestHolder analyzingSuggestHolder = lookupMap.get(mapper.names().indexName());
|
||||||
if (analyzingSuggestHolder == null) {
|
if (analyzingSuggestHolder == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int flags = analyzingSuggestHolder.preserveSep ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
int flags = analyzingSuggestHolder.getPreserveSeparator() ? XAnalyzingSuggester.PRESERVE_SEP : 0;
|
||||||
|
|
||||||
|
final Automaton queryPrefix = mapper.requiresContext() ? ContextQuery.toAutomaton(analyzingSuggestHolder.getPreserveSeparator(), suggestionContext.getContextQueries()) : null;
|
||||||
|
|
||||||
XAnalyzingSuggester suggester;
|
XAnalyzingSuggester suggester;
|
||||||
if (suggestionContext.isFuzzy()) {
|
if (suggestionContext.isFuzzy()) {
|
||||||
suggester = new XFuzzySuggester(mapper.indexAnalyzer(), mapper.searchAnalyzer(), flags,
|
suggester = new XFuzzySuggester(mapper.indexAnalyzer(),queryPrefix, mapper.searchAnalyzer(), flags,
|
||||||
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
||||||
suggestionContext.getFuzzyEditDistance(), suggestionContext.isFuzzyTranspositions(),
|
suggestionContext.getFuzzyEditDistance(), suggestionContext.isFuzzyTranspositions(),
|
||||||
suggestionContext.getFuzzyPrefixLength(), suggestionContext.getFuzzyMinLength(), false,
|
suggestionContext.getFuzzyPrefixLength(), suggestionContext.getFuzzyMinLength(), false,
|
||||||
analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
||||||
analyzingSuggestHolder.maxAnalyzedPathsForOneInput, SEP_LABEL, PAYLOAD_SEP, END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
analyzingSuggestHolder.maxAnalyzedPathsForOneInput, SEP_LABEL, PAYLOAD_SEP, END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
suggester = new XAnalyzingSuggester(mapper.indexAnalyzer(), mapper.searchAnalyzer(), flags,
|
suggester = new XAnalyzingSuggester(mapper.indexAnalyzer(), queryPrefix, mapper.searchAnalyzer(), flags,
|
||||||
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
analyzingSuggestHolder.maxSurfaceFormsPerAnalyzedForm, analyzingSuggestHolder.maxGraphExpansions,
|
||||||
analyzingSuggestHolder.preservePositionIncrements,
|
analyzingSuggestHolder.preservePositionIncrements,
|
||||||
analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
analyzingSuggestHolder.fst, analyzingSuggestHolder.hasPayloads,
|
||||||
|
@ -294,7 +294,7 @@ public class AnalyzingCompletionLookupProviderV1 extends CompletionLookupProvide
|
||||||
return new CompletionStats(sizeInBytes, completionFields);
|
return new CompletionStats(sizeInBytes, completionFields);
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
AnalyzingSuggestHolder getAnalyzingSuggestHolder(FieldMapper<?> mapper) {
|
AnalyzingSuggestHolder getAnalyzingSuggestHolder(CompletionFieldMapper mapper) {
|
||||||
return lookupMap.get(mapper.names().indexName());
|
return lookupMap.get(mapper.names().indexName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.elasticsearch.index.mapper.core.CompletionFieldMapper;
|
||||||
import org.elasticsearch.index.merge.Merges;
|
import org.elasticsearch.index.merge.Merges;
|
||||||
import org.elasticsearch.search.suggest.SuggestUtils;
|
import org.elasticsearch.search.suggest.SuggestUtils;
|
||||||
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.LookupFactory;
|
import org.elasticsearch.search.suggest.completion.Completion090PostingsFormat.LookupFactory;
|
||||||
|
import org.elasticsearch.search.suggest.context.ContextMapping;
|
||||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
LookupFactory load = currentProvider.load(input);
|
LookupFactory load = currentProvider.load(input);
|
||||||
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
||||||
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
||||||
Lookup lookup = load.getLookup(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null), new CompletionSuggestionContext(null));
|
Lookup lookup = load.getLookup(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null, ContextMapping.EMPTY_MAPPING), new CompletionSuggestionContext(null));
|
||||||
List<LookupResult> result = lookup.lookup("ge", false, 10);
|
List<LookupResult> result = lookup.lookup("ge", false, 10);
|
||||||
assertThat(result.get(0).key.toString(), equalTo("Generator - Foo Fighters"));
|
assertThat(result.get(0).key.toString(), equalTo("Generator - Foo Fighters"));
|
||||||
assertThat(result.get(0).payload.utf8ToString(), equalTo("id:10"));
|
assertThat(result.get(0).payload.utf8ToString(), equalTo("id:10"));
|
||||||
|
@ -91,7 +92,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
LookupFactory load = currentProvider.load(input);
|
LookupFactory load = currentProvider.load(input);
|
||||||
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
||||||
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
||||||
AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder analyzingSuggestHolder = load.getAnalyzingSuggestHolder(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null));
|
AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder analyzingSuggestHolder = load.getAnalyzingSuggestHolder(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null, ContextMapping.EMPTY_MAPPING));
|
||||||
assertThat(analyzingSuggestHolder.sepLabel, is(AnalyzingCompletionLookupProviderV1.SEP_LABEL));
|
assertThat(analyzingSuggestHolder.sepLabel, is(AnalyzingCompletionLookupProviderV1.SEP_LABEL));
|
||||||
assertThat(analyzingSuggestHolder.payloadSep, is(AnalyzingCompletionLookupProviderV1.PAYLOAD_SEP));
|
assertThat(analyzingSuggestHolder.payloadSep, is(AnalyzingCompletionLookupProviderV1.PAYLOAD_SEP));
|
||||||
assertThat(analyzingSuggestHolder.endByte, is(AnalyzingCompletionLookupProviderV1.END_BYTE));
|
assertThat(analyzingSuggestHolder.endByte, is(AnalyzingCompletionLookupProviderV1.END_BYTE));
|
||||||
|
@ -109,7 +110,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
LookupFactory load = currentProvider.load(input);
|
LookupFactory load = currentProvider.load(input);
|
||||||
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
||||||
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
||||||
AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder analyzingSuggestHolder = load.getAnalyzingSuggestHolder(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null));
|
AnalyzingCompletionLookupProvider.AnalyzingSuggestHolder analyzingSuggestHolder = load.getAnalyzingSuggestHolder(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null, ContextMapping.EMPTY_MAPPING));
|
||||||
assertThat(analyzingSuggestHolder.sepLabel, is(XAnalyzingSuggester.SEP_LABEL));
|
assertThat(analyzingSuggestHolder.sepLabel, is(XAnalyzingSuggester.SEP_LABEL));
|
||||||
assertThat(analyzingSuggestHolder.payloadSep, is(XAnalyzingSuggester.PAYLOAD_SEP));
|
assertThat(analyzingSuggestHolder.payloadSep, is(XAnalyzingSuggester.PAYLOAD_SEP));
|
||||||
assertThat(analyzingSuggestHolder.endByte, is(XAnalyzingSuggester.END_BYTE));
|
assertThat(analyzingSuggestHolder.endByte, is(XAnalyzingSuggester.END_BYTE));
|
||||||
|
@ -124,7 +125,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
final boolean usePayloads = getRandom().nextBoolean();
|
final boolean usePayloads = getRandom().nextBoolean();
|
||||||
final int options = preserveSeparators ? AnalyzingSuggester.PRESERVE_SEP : 0;
|
final int options = preserveSeparators ? AnalyzingSuggester.PRESERVE_SEP : 0;
|
||||||
|
|
||||||
XAnalyzingSuggester reference = new XAnalyzingSuggester(new StandardAnalyzer(TEST_VERSION_CURRENT), new StandardAnalyzer(
|
XAnalyzingSuggester reference = new XAnalyzingSuggester(new StandardAnalyzer(TEST_VERSION_CURRENT), null, new StandardAnalyzer(
|
||||||
TEST_VERSION_CURRENT), options, 256, -1, preservePositionIncrements, null, false, 1, XAnalyzingSuggester.SEP_LABEL, XAnalyzingSuggester.PAYLOAD_SEP, XAnalyzingSuggester.END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
TEST_VERSION_CURRENT), options, 256, -1, preservePositionIncrements, null, false, 1, XAnalyzingSuggester.SEP_LABEL, XAnalyzingSuggester.PAYLOAD_SEP, XAnalyzingSuggester.END_BYTE, XAnalyzingSuggester.HOLE_CHARACTER);
|
||||||
LineFileDocs docs = new LineFileDocs(getRandom());
|
LineFileDocs docs = new LineFileDocs(getRandom());
|
||||||
int num = atLeast(150);
|
int num = atLeast(150);
|
||||||
|
@ -208,7 +209,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
|
|
||||||
NamedAnalyzer namedAnalzyer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
NamedAnalyzer namedAnalzyer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
||||||
final CompletionFieldMapper mapper = new CompletionFieldMapper(new Names("foo"), namedAnalzyer, namedAnalzyer, provider, null, usePayloads,
|
final CompletionFieldMapper mapper = new CompletionFieldMapper(new Names("foo"), namedAnalzyer, namedAnalzyer, provider, null, usePayloads,
|
||||||
preserveSeparators, preservePositionIncrements, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null);
|
preserveSeparators, preservePositionIncrements, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null, ContextMapping.EMPTY_MAPPING);
|
||||||
Lookup buildAnalyzingLookup = buildAnalyzingLookup(mapper, titles, titles, weights);
|
Lookup buildAnalyzingLookup = buildAnalyzingLookup(mapper, titles, titles, weights);
|
||||||
Field field = buildAnalyzingLookup.getClass().getDeclaredField("maxAnalyzedPathsForOneInput");
|
Field field = buildAnalyzingLookup.getClass().getDeclaredField("maxAnalyzedPathsForOneInput");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
|
@ -259,7 +260,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
for (int i = 0; i < weights.length; i++) {
|
for (int i = 0; i < weights.length; i++) {
|
||||||
Document doc = new Document();
|
Document doc = new Document();
|
||||||
BytesRef payload = mapper.buildPayload(new BytesRef(surfaces[i]), weights[i], new BytesRef(Long.toString(weights[i])));
|
BytesRef payload = mapper.buildPayload(new BytesRef(surfaces[i]), weights[i], new BytesRef(Long.toString(weights[i])));
|
||||||
doc.add(mapper.getCompletionField(terms[i], payload));
|
doc.add(mapper.getCompletionField(ContextMapping.EMPTY_CONTEXT, terms[i], payload));
|
||||||
if (randomBoolean()) {
|
if (randomBoolean()) {
|
||||||
writer.commit();
|
writer.commit();
|
||||||
}
|
}
|
||||||
|
@ -297,7 +298,7 @@ public class CompletionPostingsFormatTest extends ElasticsearchTestCase {
|
||||||
LookupFactory load = provider.load(input);
|
LookupFactory load = provider.load(input);
|
||||||
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
PostingsFormatProvider format = new PreBuiltPostingsFormatProvider(new Elasticsearch090PostingsFormat());
|
||||||
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
NamedAnalyzer analyzer = new NamedAnalyzer("foo", new StandardAnalyzer(TEST_VERSION_CURRENT));
|
||||||
assertNull(load.getLookup(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null), new CompletionSuggestionContext(null)));
|
assertNull(load.getLookup(new CompletionFieldMapper(new Names("foo"), analyzer, analyzer, format, null, true, true, true, Integer.MAX_VALUE, AbstractFieldMapper.MultiFields.empty(), null, ContextMapping.EMPTY_MAPPING), new CompletionSuggestionContext(null)));
|
||||||
dir.close();
|
dir.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,10 @@ import com.spatial4j.core.shape.Shape;
|
||||||
import com.spatial4j.core.shape.jts.JtsGeometry;
|
import com.spatial4j.core.shape.jts.JtsGeometry;
|
||||||
import com.spatial4j.core.shape.jts.JtsPoint;
|
import com.spatial4j.core.shape.jts.JtsPoint;
|
||||||
import com.vividsolutions.jts.geom.*;
|
import com.vividsolutions.jts.geom.*;
|
||||||
|
import org.elasticsearch.common.geo.GeoDistance;
|
||||||
|
import org.elasticsearch.common.geo.GeoPoint;
|
||||||
|
import org.elasticsearch.common.unit.DistanceUnit;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -201,4 +205,19 @@ public class ElasticsearchGeoAssertions {
|
||||||
public static void assertMultiLineString(Shape shape) {
|
public static void assertMultiLineString(Shape shape) {
|
||||||
assert(unwrap(shape) instanceof MultiLineString): "expected MultiLineString but found " + unwrap(shape).getClass().getName();
|
assert(unwrap(shape) instanceof MultiLineString): "expected MultiLineString but found " + unwrap(shape).getClass().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void assertDistance(String geohash1, String geohash2, Matcher<Double> match) {
|
||||||
|
GeoPoint p1 = new GeoPoint(geohash1);
|
||||||
|
GeoPoint p2 = new GeoPoint(geohash2);
|
||||||
|
assertDistance(p1.lat(), p1.lon(), p2.lat(),p2.lon(), match);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertDistance(double lat1, double lon1, double lat2, double lon2, Matcher<Double> match) {
|
||||||
|
assertThat(distance(lat1, lon1, lat2, lon2), match);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double distance(double lat1, double lon1, double lat2, double lon2) {
|
||||||
|
return GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, DistanceUnit.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue