Use `typed_keys` parameter to prefix suggester names by type in search responses (#23080)

This pull request reuses the typed_keys parameter added in #22965, but this time it applies it to suggesters. When set to true, the suggester names in the search response will be prefixed with a prefix that reflects their type.
This commit is contained in:
Tanguy Leroux 2017-02-10 10:53:38 +01:00 committed by GitHub
parent 63ea6f7168
commit e2e5937455
19 changed files with 253 additions and 24 deletions

View File

@ -52,7 +52,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST;
public class RestMultiSearchAction extends BaseRestHandler { public class RestMultiSearchAction extends BaseRestHandler {
private static final Set<String> RESPONSE_PARAMS = Collections.singleton("typed_keys"); private static final Set<String> RESPONSE_PARAMS = Collections.singleton(RestSearchAction.TYPED_KEYS_PARAM);
private final boolean allowExplicitIndex; private final boolean allowExplicitIndex;

View File

@ -54,7 +54,8 @@ import static org.elasticsearch.search.suggest.SuggestBuilders.termSuggestion;
public class RestSearchAction extends BaseRestHandler { public class RestSearchAction extends BaseRestHandler {
private static final Set<String> RESPONSE_PARAMS = Collections.singleton("typed_keys"); public static final String TYPED_KEYS_PARAM = "typed_keys";
private static final Set<String> RESPONSE_PARAMS = Collections.singleton(TYPED_KEYS_PARAM);
public RestSearchAction(Settings settings, RestController controller) { public RestSearchAction(Settings settings, RestController controller) {
super(settings); super(settings);

View File

@ -25,6 +25,7 @@ import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.BigArrays;
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.rest.action.search.RestSearchAction;
import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.search.aggregations.support.AggregationPath;
@ -164,9 +165,12 @@ public abstract class InternalAggregation implements Aggregation, ToXContent, Na
@Override @Override
public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
// Concatenates the type and the name of the aggregation (ex: top_hits#foo) if (params.paramAsBoolean(RestSearchAction.TYPED_KEYS_PARAM, false)) {
String name = params.paramAsBoolean("typed_keys", false) ? String.join(TYPED_KEYS_DELIMITER, getType(), getName()) : getName(); // Concatenates the type and the name of the aggregation (ex: top_hits#foo)
builder.startObject(name); builder.startObject(String.join(TYPED_KEYS_DELIMITER, getType(), getName()));
} else {
builder.startObject(getName());
}
if (this.metaData != null) { if (this.metaData != null) {
builder.field(CommonFields.META); builder.field(CommonFields.META);
builder.map(this.metaData); builder.map(this.metaData);

View File

@ -30,6 +30,8 @@ import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.action.search.RestSearchAction;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry; import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry;
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option; import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry.Option;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion; import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
@ -149,7 +151,7 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
public void writeTo(StreamOutput out) throws IOException { public void writeTo(StreamOutput out) throws IOException {
out.writeVInt(suggestions.size()); out.writeVInt(suggestions.size());
for (Suggestion<?> command : suggestions) { for (Suggestion<?> command : suggestions) {
out.writeVInt(command.getType()); out.writeVInt(command.getWriteableType());
command.writeTo(out); command.writeTo(out);
} }
} }
@ -206,6 +208,8 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
*/ */
public static class Suggestion<T extends Suggestion.Entry> implements Iterable<T>, Streamable, ToXContent { public static class Suggestion<T extends Suggestion.Entry> implements Iterable<T>, Streamable, ToXContent {
private static final String NAME = "suggestion";
public static final int TYPE = 0; public static final int TYPE = 0;
protected String name; protected String name;
protected int size; protected int size;
@ -223,10 +227,23 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
entries.add(entry); entries.add(entry);
} }
public int getType() { /**
* Returns a integer representing the type of the suggestion. This is used for
* internal serialization over the network.
*/
public int getWriteableType() { // TODO remove this in favor of NamedWriteable
return TYPE; return TYPE;
} }
/**
* Returns a string representing the type of the suggestion. This type is added to
* the suggestion name in the XContent response, so that it can later be used by
* REST clients to determine the internal type of the suggestion.
*/
protected String getType() {
return NAME;
}
@Override @Override
public Iterator<T> iterator() { public Iterator<T> iterator() {
return entries.iterator(); return entries.iterator();
@ -338,7 +355,12 @@ public class Suggest implements Iterable<Suggest.Suggestion<? extends Entry<? ex
@Override @Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startArray(name); if (params.paramAsBoolean(RestSearchAction.TYPED_KEYS_PARAM, false)) {
// Concatenates the type and the name of the suggestion (ex: completion#foo)
builder.startArray(String.join(InternalAggregation.TYPED_KEYS_DELIMITER, getType(), getName()));
} else {
builder.startArray(getName());
}
for (Entry<?> entry : entries) { for (Entry<?> entry : entries) {
entry.toXContent(builder, params); entry.toXContent(builder, params);
} }

View File

@ -57,6 +57,8 @@ import static org.elasticsearch.search.suggest.Suggest.COMPARATOR;
*/ */
public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSuggestion.Entry> { public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSuggestion.Entry> {
private static final String NAME = "completion";
public static final int TYPE = 4; public static final int TYPE = 4;
public CompletionSuggestion() { public CompletionSuggestion() {
@ -165,10 +167,15 @@ public final class CompletionSuggestion extends Suggest.Suggestion<CompletionSug
} }
@Override @Override
public int getType() { public int getWriteableType() {
return TYPE; return TYPE;
} }
@Override
protected String getType() {
return NAME;
}
@Override @Override
protected Entry newEntry() { protected Entry newEntry() {
return new Entry(); return new Entry();

View File

@ -31,6 +31,8 @@ import java.io.IOException;
* Suggestion entry returned from the {@link PhraseSuggester}. * Suggestion entry returned from the {@link PhraseSuggester}.
*/ */
public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry> { public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry> {
private static final String NAME = "phrase";
public static final int TYPE = 3; public static final int TYPE = 3;
public PhraseSuggestion() { public PhraseSuggestion() {
@ -41,10 +43,15 @@ public class PhraseSuggestion extends Suggest.Suggestion<PhraseSuggestion.Entry>
} }
@Override @Override
public int getType() { public int getWriteableType() {
return TYPE; return TYPE;
} }
@Override
protected String getType() {
return NAME;
}
@Override @Override
protected Entry newEntry() { protected Entry newEntry() {
return new Entry(); return new Entry();

View File

@ -40,6 +40,8 @@ import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constru
*/ */
public class TermSuggestion extends Suggestion<TermSuggestion.Entry> { public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
private static final String NAME = "term";
public static final Comparator<Suggestion.Entry.Option> SCORE = new Score(); public static final Comparator<Suggestion.Entry.Option> SCORE = new Score();
public static final Comparator<Suggestion.Entry.Option> FREQUENCY = new Frequency(); public static final Comparator<Suggestion.Entry.Option> FREQUENCY = new Frequency();
public static final int TYPE = 1; public static final int TYPE = 1;
@ -96,10 +98,15 @@ public class TermSuggestion extends Suggestion<TermSuggestion.Entry> {
} }
@Override @Override
public int getType() { public int getWriteableType() {
return TYPE; return TYPE;
} }
@Override
protected String getType() {
return NAME;
}
@Override @Override
protected Comparator<Option> sortComparator() { protected Comparator<Option> sortComparator() {
switch (sort) { switch (sort) {

View File

@ -118,8 +118,8 @@ GET /twitter/tweet/_search?typed_keys
// CONSOLE // CONSOLE
// TEST[setup:twitter] // TEST[setup:twitter]
In the response, the aggregations names will be changed to respectively `date_histogram:tweets_over_time` and In the response, the aggregations names will be changed to respectively `date_histogram#tweets_over_time` and
`top_hits:top_users`, reflecting the internal types of each aggregation: `top_hits#top_users`, reflecting the internal types of each aggregation:
[source,js] [source,js]
-------------------------------------------------- --------------------------------------------------

View File

@ -147,3 +147,5 @@ include::suggesters/phrase-suggest.asciidoc[]
include::suggesters/completion-suggest.asciidoc[] include::suggesters/completion-suggest.asciidoc[]
include::suggesters/context-suggest.asciidoc[] include::suggesters/context-suggest.asciidoc[]
include::suggesters/misc.asciidoc[]

View File

@ -0,0 +1,84 @@
[[returning-suggesters-type]]
=== Returning the type of the suggester
Sometimes you need to know the exact type of a suggester in order to parse its results. The `typed_keys` parameter
can be used to change the suggester's name in the response so that it will be prefixed by its type.
Considering the following example with two suggesters `term` and `phrase`:
[source,js]
--------------------------------------------------
POST _search?typed_keys
{
"suggest": {
"text" : "some test mssage",
"my-first-suggester" : {
"term" : {
"field" : "message"
}
},
"my-second-suggester" : {
"phrase" : {
"field" : "message"
}
}
}
}
--------------------------------------------------
// CONSOLE
// TEST[setup:twitter]
In the response, the suggester names will be changed to respectively `term#my-first-suggester` and
`phrase#my-second-suggester`, reflecting the types of each suggestion:
[source,js]
--------------------------------------------------
{
"suggest": {
"term#my-first-suggester": [ <1>
{
"text": "some",
"offset": 0,
"length": 4,
"options": []
},
{
"text": "test",
"offset": 5,
"length": 4,
"options": []
},
{
"text": "mssage",
"offset": 10,
"length": 6,
"options": [
{
"text": "message",
"score": 0.8333333,
"freq": 4
}
]
}
],
"phrase#my-second-suggester": [ <2>
{
"text": "some test mssage",
"offset": 0,
"length": 16,
"options": [
{
"text": "some test message",
"score": 0.030227963
}
]
}
]
},
...
}
--------------------------------------------------
// TESTRESPONSE[s/\.\.\./"took": "$body.took", "timed_out": false, "_shards": "$body._shards", "hits": "$body.hits"/]
<1> The name `my-first-suggester` now contains the `term` prefix.
<2> The name `my-second-suggester` now contains the `phrase` prefix.

View File

@ -28,6 +28,7 @@ import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.rest.action.RestToXContentListener;
import org.elasticsearch.rest.action.search.RestMultiSearchAction; import org.elasticsearch.rest.action.search.RestMultiSearchAction;
import org.elasticsearch.rest.action.search.RestSearchAction;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
@ -38,7 +39,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST;
public class RestMultiSearchTemplateAction extends BaseRestHandler { public class RestMultiSearchTemplateAction extends BaseRestHandler {
private static final Set<String> RESPONSE_PARAMS = Collections.singleton("typed_keys"); private static final Set<String> RESPONSE_PARAMS = Collections.singleton(RestSearchAction.TYPED_KEYS_PARAM);
private final boolean allowExplicitIndex; private final boolean allowExplicitIndex;

View File

@ -45,7 +45,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST;
public class RestSearchTemplateAction extends BaseRestHandler { public class RestSearchTemplateAction extends BaseRestHandler {
private static final Set<String> RESPONSE_PARAMS = Collections.singleton("typed_keys"); private static final Set<String> RESPONSE_PARAMS = Collections.singleton(RestSearchAction.TYPED_KEYS_PARAM);
private static final ObjectParser<SearchTemplateRequest, Void> PARSER; private static final ObjectParser<SearchTemplateRequest, Void> PARSER;
static { static {

View File

@ -48,6 +48,11 @@ setup:
test_missing: test_missing:
missing: missing:
field: "{{missing_field}}" field: "{{missing_field}}"
suggest:
term_suggest:
text: "{{suggest_text}}"
term:
field: "{{suggest_field}}"
- match: { acknowledged: true } - match: { acknowledged: true }
@ -60,9 +65,12 @@ setup:
params: params:
bool_value: true bool_value: true
missing_field: name missing_field: name
suggest_field: name
suggest_text: Hamilt
- match: { hits.total: 3 } - match: { hits.total: 3 }
- match: { aggregations.missing#test_missing.doc_count: 1 } - match: { aggregations.missing#test_missing.doc_count: 1 }
- is_true: suggest.term#term_suggest
--- ---
"Multisearch template with typed_keys parameter": "Multisearch template with typed_keys parameter":
@ -81,6 +89,11 @@ setup:
histogram: histogram:
field: "{{histo.field}}" field: "{{histo.field}}"
interval: "{{histo.interval}}" interval: "{{histo.interval}}"
suggest:
phrase_suggester:
text: "{{keywords}}"
phrase:
field: name
- match: { acknowledged: true } - match: { acknowledged: true }
@ -112,8 +125,11 @@ setup:
histo: histo:
field: float field: float
interval: 5 interval: 5
keywords: Ruht
- match: { responses.0.hits.total: 1 } - match: { responses.0.hits.total: 1 }
- match: { responses.0.aggregations.global#test_global.doc_count: 5 } - match: { responses.0.aggregations.global#test_global.doc_count: 5 }
- match: { responses.0.aggregations.global#test_global.ip_range#test_ip_range.buckets.0.doc_count: 5 } - match: { responses.0.aggregations.global#test_global.ip_range#test_ip_range.buckets.0.doc_count: 5 }
- match: { responses.1.hits.total: 2 } - match: { responses.1.hits.total: 2 }
- match: { responses.1.aggregations.histogram#test_histogram.buckets.0.doc_count: 1 } - match: { responses.1.aggregations.histogram#test_histogram.buckets.0.doc_count: 1 }
- is_true: responses.1.suggest.phrase#phrase_suggester

View File

@ -27,7 +27,7 @@
}, },
"typed_keys": { "typed_keys": {
"type" : "boolean", "type" : "boolean",
"description" : "Specify whether aggregation names should be prefixed by their respective types in the response" "description" : "Specify whether aggregation and suggester names should be prefixed by their respective types in the response"
} }
} }
}, },

View File

@ -23,7 +23,7 @@
}, },
"typed_keys": { "typed_keys": {
"type" : "boolean", "type" : "boolean",
"description" : "Specify whether aggregation names should be prefixed by their respective types in the response" "description" : "Specify whether aggregation and suggester names should be prefixed by their respective types in the response"
} }
} }
}, },

View File

@ -149,7 +149,7 @@
}, },
"typed_keys": { "typed_keys": {
"type" : "boolean", "type" : "boolean",
"description" : "Specify whether aggregation names should be prefixed by their respective types in the response" "description" : "Specify whether aggregation and suggester names should be prefixed by their respective types in the response"
}, },
"version": { "version": {
"type" : "boolean", "type" : "boolean",

View File

@ -57,7 +57,7 @@
}, },
"typed_keys": { "typed_keys": {
"type" : "boolean", "type" : "boolean",
"description" : "Specify whether aggregation names should be prefixed by their respective types in the response" "description" : "Specify whether aggregation and suggester names should be prefixed by their respective types in the response"
} }
} }
}, },

View File

@ -21,19 +21,20 @@ setup:
type: float type: float
name: name:
type: keyword type: keyword
title:
type: completion
- do: - do:
bulk: bulk:
refresh: true refresh: true
body: body:
- '{"index": {"_index": "test-0", "_type": "user"}}' - '{"index": {"_index": "test-0", "_type": "user"}}'
- '{"row": 1, "index_start_at": 56, "integer": 38, "float": 12.5713, "name": "Ruth", "bool": true}' - '{"row": 1, "index_start_at": 56, "integer": 38, "float": 12.5713, "name": "Ruth", "bool": true, "title": "doctor"}'
- '{"index": {"_index": "test-0", "_type": "user"}}' - '{"index": {"_index": "test-0", "_type": "user"}}'
- '{"row": 2, "index_start_at": 57, "integer": 42, "float": 15.3393, "name": "Jackie", "surname": "Bowling", "bool": false}' - '{"row": 2, "index_start_at": 57, "integer": 42, "float": 15.3393, "name": "Jackie", "surname": "Bowling", "bool": false}'
- '{"index": {"_index": "test-1", "_type": "user"}}' - '{"index": {"_index": "test-1", "_type": "user"}}'
- '{"row": 3, "index_start_at": 58, "integer": 29, "float": 19.0517, "name": "Stephanie", "bool": true}' - '{"row": 3, "index_start_at": 58, "integer": 29, "float": 19.0517, "name": "Stephanie", "bool": true}'
- '{"index": {"_index": "test-1", "_type": "user"}}' - '{"index": {"_index": "test-1", "_type": "user"}}'
- '{"row": 4, "index_start_at": 59, "integer": 19, "float": 19.3717, "surname": "Hamilton", "bool": true}' - '{"row": 4, "index_start_at": 59, "integer": 19, "float": 19.3717, "surname": "Hamilton", "bool": true, "title": "commandant"}'
- '{"index": {"_index": "test-2", "_type": "user"}}' - '{"index": {"_index": "test-2", "_type": "user"}}'
- '{"row": 5, "index_start_at": 60, "integer": 0, "float": 17.3349, "name": "Natalie", "bool": false}' - '{"row": 5, "index_start_at": 60, "integer": 0, "float": 17.3349, "name": "Natalie", "bool": false}'
@ -43,12 +44,20 @@ setup:
msearch: msearch:
typed_keys: true typed_keys: true
body: body:
# Testing aggegrations
- index: test-* - index: test-*
- {query: {match: {bool: true} }, size: 0, aggs: {test_filter: {filter: {range:{integer: {gte: 20} } } } } } - {query: {match: {bool: true} }, size: 0, aggs: {test_filter: {filter: {range:{integer: {gte: 20} } } } } }
- index: test-1 - index: test-1
- {query: {match_all: {} }, size: 0, aggs: {test_range: {range: {field: float, ranges: [ {to: 19.2499999}, {from: 19.25} ] } } } } - {query: {match_all: {} }, size: 0, aggs: {test_range: {range: {field: float, ranges: [ {to: 19.2499999}, {from: 19.25} ] } } } }
- index: test-* - index: test-*
- {query: {bool: {filter: {range: {row: {lt: 5}}} } }, size: 0, aggs: {test_percentiles: {percentiles: {field: float} } } } - {query: {bool: {filter: {range: {row: {lt: 5}}} } }, size: 0, aggs: {test_percentiles: {percentiles: {field: float} } } }
# Testing suggesters
- index: test-*
- {query: {match_all: {} }, size: 0, suggest: {term_suggester: {text: Natalie, term: {field: name } } } }
- index: test-*
- {query: {match_all: {} }, size: 0, suggest: {completion_suggester: {prefix: doc, completion: {field: title } } } }
- index: test-*
- {query: {match_all: {} }, size: 0, suggest: {phrase_suggester: {text: Ruht, phrase: {field: name } } } }
- match: { responses.0.hits.total: 3 } - match: { responses.0.hits.total: 3 }
- match: { responses.0.aggregations.filter#test_filter.doc_count : 2 } - match: { responses.0.aggregations.filter#test_filter.doc_count : 2 }
@ -59,6 +68,9 @@ setup:
- match: { responses.1.aggregations.range#test_range.buckets.1.doc_count : 1 } - match: { responses.1.aggregations.range#test_range.buckets.1.doc_count : 1 }
- match: { responses.2.hits.total: 4 } - match: { responses.2.hits.total: 4 }
- is_true: responses.2.aggregations.tdigest_percentiles#test_percentiles.values - is_true: responses.2.aggregations.tdigest_percentiles#test_percentiles.values
- is_true: responses.3.suggest.term#term_suggester
- is_true: responses.4.suggest.completion#completion_suggester
- is_true: responses.5.suggest.phrase#phrase_suggester
--- ---
"Multisearch test with typed_keys parameter for sampler and significant terms": "Multisearch test with typed_keys parameter for sampler and significant terms":

View File

@ -0,0 +1,66 @@
setup:
- skip:
version: " - 5.3.99"
reason: typed_keys parameter was added in 5.4.0
- do:
indices.create:
index: test
body:
settings:
number_of_replicas: 0
mappings:
test:
properties:
title:
type: keyword
suggestions:
type: completion
contexts:
-
"name" : "format"
"type" : "category"
- do:
bulk:
refresh: true
index: test
type: test
body:
- '{"index": {}}'
- '{"title": "Elasticsearch in Action", "suggestions": {"input": "ELK in Action", "contexts": {"format": "ebook"}}}'
- '{"index": {}}'
- '{"title": "Elasticsearch - The Definitive Guide", "suggestions": {"input": ["Elasticsearch in Action"]}}'
---
"Test typed keys parameter for suggesters":
- do:
search:
typed_keys: true
body:
query:
match_all: {}
suggest:
text: "Elastic"
term_suggester:
term:
field: title
completion_suggester:
prefix: "Elastic"
completion:
field: suggestions
context_suggester:
prefix: "Elastic"
completion:
field: suggestions
contexts:
format: "ebook"
phrase_suggester:
phrase:
field: title
- is_true: suggest.term#term_suggester
- is_true: suggest.completion#completion_suggester
- is_true: suggest.completion#context_suggester
- is_true: suggest.phrase#phrase_suggester