Skip to content

Commit

Permalink
Added suggest api.
Browse files Browse the repository at this point in the history
# Suggest feature
The suggest feature suggests similar looking terms based on a provided text by using a suggester. At the moment there the only supported suggester is `fuzzy`. The suggest feature is available since version `0.21.0`.

# Fuzzy suggester
The `fuzzy` suggester suggests terms based on edit distance. The provided suggest text is analyzed before terms are suggested. The suggested terms are provided per analyzed suggest text token. The `fuzzy` suggester doesn't take the query into account that is part of request.

# Suggest API
The suggest request part is defined along side the query part as top field in the json request.

```
curl -s -XPOST 'localhost:9200/_search' -d '{
    "query" : {
        ...
    },
    "suggest" : {
        ...
    }
}'
```

Several suggestions can be specified per request. Each suggestion is identified with an arbitary name. In the example below two suggestions are requested. The `my-suggest-1` suggestion uses the `body` field and `my-suggest-2` uses the `title` field. The `type` field is a required field and defines what suggester to use for a suggestion.

```
"suggest" : {
    "suggestions" : {
        "my-suggest-1" : {
            "type" : "fuzzy",
            "field" : "body",
            "text" : "the amsterdma meetpu"
        },
        "my-suggest-2" : {
            "type" : "fuzzy",
            "field" : "title",
            "text" : "the rottredam meetpu"
        }
    }
}
```

The below suggest response example includes the suggestions part for `my-suggest-1` and `my-suggest-2`. Each suggestion part contains a terms array, that contains all terms outputted by the analyzed suggest text. Each term object includes the term itself, the original start and end offset in the suggest text and if found an arbitary number of suggestions.

```
{
    ...
    "suggest": {
        "my-suggest-1": {
            "terms" : [
              {
                "term" : "amsterdma",
                "start_offset": 5,
                "end_offset": 14,
                "suggestions": [
                   ...
                ]
              }
              ...
            ]
        },
        "my-suggest-2" : {
          "terms" : [
            ...
          ]
        }
    }
```

Each suggestions array contains a suggestion object that includes the suggested term, its document frequency and score compared to the suggest text term. The meaning of the score depends on the used suggester. The fuzzy suggester's score is based on the edit distance.

```
"suggestions": [
    {
        "term": "amsterdam",
        "frequency": 77,
        "score": 0.8888889
    },
    ...
]
```

# Global suggest text

To avoid repitition of the suggest text, it is possible to define a global text. In the example below the suggest text is a global option and applies to the `my-suggest-1` and `my-suggest-2` suggestions.

```
"suggest" : {
    "suggestions" : {
        "text" : "the amsterdma meetpu",
        "my-suggest-1" : {
            "type" : "fuzzy",
            "field" : "title"
        },
        "my-suggest-2" : {
            "type" : "fuzzy",
            "field" : "body"
        }
    }
}
```

The suggest text can be specied as global option or as suggestion specific option. The suggest text specified on suggestion level override the suggest text on the global level.

# Other suggest example.

In the below example we request suggestions for the following suggest text: `devloping distibutd saerch engies` on the `title` field with a maximum of 3 suggestions per term inside the suggest text. Note that in this example we use the `count` search type. This isn't required, but a nice optimalization. The suggestions are gather in the `query` phase and in the case that we only care about suggestions (so no hits) we don't need to execute the `fetch` phase.

```
curl -s -XPOST 'localhost:9200/_search?search_type=count' -d '{
  "suggest" : {
      "suggestions" : {
        "my-title-suggestions" : {
          "suggester" : "fuzzy",
          "field" : "title",
          "text" : "devloping distibutd saerch engies",
          "size" : 3
        }
      }
  }
}'
```

The above request could yield the response as stated in the code example below. As you can see if we take the first suggested term of each suggest text term we get `developing distributed search engines` as result.

```
{
  ...
  "suggest": {
    "my-title-suggestions": {
      "terms": [
        {
          "term": "devloping",
          "start_offset": 0,
          "end_offset": 9,
          "suggestions": [
            {
              "term": "developing",
              "frequency": 77,
              "score": 0.8888889
            },
            {
              "term": "deloping",
              "frequency": 1,
              "score": 0.875
            },
            {
              "term": "deploying",
              "frequency": 2,
              "score": 0.7777778
            }
          ]
        },
        {
          "term": "distibutd",
          "start_offset": 10,
          "end_offset": 19,
          "suggestions": [
            {
              "term": "distributed",
              "frequency": 217,
              "score": 0.7777778
            },
            {
              "term": "disributed",
              "frequency": 1,
              "score": 0.7777778
            },
            {
              "term": "distribute",
              "frequency": 1,
              "score": 0.7777778
            }
          ]
        },
        {
          "term": "saerch",
          "start_offset": 20,
          "end_offset": 26,
          "suggestions": [
            {
              "term": "search",
              "frequency": 1038,
              "score": 0.8333333
            },
            {
              "term": "smerch",
              "frequency": 3,
              "score": 0.8333333
            },
            {
              "term": "serch",
              "frequency": 2,
              "score": 0.8
            }
          ]
        },
        {
          "term": "engies",
          "start_offset": 27,
          "end_offset": 33,
          "suggestions": [
            {
              "term": "engines",
              "frequency": 568,
              "score": 0.8333333
            },
            {
              "term": "engles",
              "frequency": 3,
              "score": 0.8333333
            },
            {
              "term": "eggies",
              "frequency": 1,
              "score": 0.8333333
            }
          ]
        }
      ]
    }
  }
  ...
}
```

# Common suggest options:
* `suggester` - The suggester implementation type. The only supported value is 'fuzzy'. This is a required option.
* `text` - The suggest text. The suggest text is a required option that needs to be set globally or per suggestion.

# Common fuzzy suggest options
* `field` - The field to fetch the candidate suggestions from. This is an required option that either needs to be set globally or per suggestion.
* `analyzer` - The analyzer to analyse the suggest text with. Defaults to the search analyzer of the suggest field.
* `size` - The maximum corrections to be returned per suggest text token.
* `sort` - Defines how suggestions should be sorted per suggest text term. Two possible value:
** `score` - Sort by sore first, then document frequency and then the term itself.
** `frequency` - Sort by document frequency first, then simlarity score and then the term itself.
* `suggest_mode` - The suggest mode controls what suggestions are included or controls for what suggest text terms, suggestions should be suggested. Three possible values can be specified:
** `missing` - Only suggest terms in the suggest text that aren't in the index. This is the default.
** `popular` - Only suggest suggestions that occur in more docs then the original suggest text term.
** `always` - Suggest any matching suggestions based on terms in the suggest text.

# Other fuzzy suggest options:
* `lowercase_terms` - Lower cases the suggest text terms after text analyzation.
* `max_edits` - The maximum edit distance candidate suggestions can have in order to be considered as a suggestion. Can only be a value between 1 and 2. Any other value result in an bad request error being thrown. Defaults to 2.
* `min_prefix` - The number of minimal prefix characters that must match in order be a candidate suggestions. Defaults to 1. Increasing this number improves spellcheck performance. Usually misspellings don't occur in the beginning of terms.
* `min_query_length` -  The minimum length a suggest text term must have in order to be included. Defaults to 4.
* `shard_size` - Sets the maximum number of suggestions to be retrieved from each individual shard. During the reduce phase only the top N suggestions are returned based on the `size` option. Defaults to the `size` option. Setting this to a value higher than the `size` can be useful in order to get a more accurate document frequency for spelling corrections at the cost of performance. Due to the fact that terms are partitioned amongst shards, the shard level document frequencies of spelling corrections may not be precise. Increasing this will make these document frequencies more precise.
* `max_inspections` - A factor that is used to multiply with the `shards_size` in order to inspect more candidate spell corrections on the shard level. Can improve accuracy at the cost of performance. Defaults to 5.
* `threshold_frequency` - The minimal threshold in number of documents a suggestion should appear in. This can be specified as an absolute number or as a relative percentage of number of documents. This can improve quality by only suggesting high frequency terms. Defaults to 0f and is not enabled. If a value higher than 1 is specified then the number cannot be fractional. The shard level document frequencies are used for this option.
* `max_query_frequency` - The maximum threshold in number of documents a sugges text token can exist in order to be included. Can be a relative percentage number (e.g 0.4) or an absolute number to represent document frequencies. If an value higher than 1 is specified then fractional can not be specified. Defaults to 0.01f. This can be used to exclude high frequency terms from being spellchecked. High frequency terms are usually spelled correctly on top of this this also improves the spellcheck performance.  The shard level document frequencies are used for this option.

 Closes elastic#2585
  • Loading branch information
martijnvg committed Jan 24, 2013
1 parent 6f7253c commit 32e4a58
Show file tree
Hide file tree
Showing 18 changed files with 2,233 additions and 6 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Expand Up @@ -91,6 +91,12 @@
<version>${lucene.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-suggest</artifactId>
<version>${lucene.version}</version>
<scope>compile</scope>
</dependency>


<!-- START: dependencies that are shaded -->
Expand Down
Expand Up @@ -37,6 +37,7 @@
import org.elasticsearch.search.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SuggestBuilder;

import java.util.Map;

Expand Down Expand Up @@ -646,6 +647,22 @@ public SearchRequestBuilder setHighlighterType(String type) {
return this;
}

/**
* Delegates to {@link org.elasticsearch.search.suggest.SuggestBuilder#setText(String)}.
*/
public SearchRequestBuilder setSuggestText(String globalText) {
suggestBuilder().setText(globalText);
return this;
}

/**
* Delegates to {@link org.elasticsearch.search.suggest.SuggestBuilder#addSuggestion(org.elasticsearch.search.suggest.SuggestBuilder.Suggestion)}.
*/
public SearchRequestBuilder addSuggestion(SuggestBuilder.Suggestion suggestion) {
suggestBuilder().addSuggestion(suggestion);
return this;
}

/**
* Sets the source of the request as a json string. Note, settings anything other
* than the search type will cause this source to be overridden, consider using
Expand Down Expand Up @@ -818,4 +835,9 @@ private SearchSourceBuilder sourceBuilder() {
private HighlightBuilder highlightBuilder() {
return sourceBuilder().highlighter();
}

private SuggestBuilder suggestBuilder() {
return sourceBuilder().suggest();
}

}
Expand Up @@ -31,6 +31,7 @@
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.facet.Facets;
import org.elasticsearch.search.internal.InternalSearchResponse;
import org.elasticsearch.search.suggest.Suggest;

import java.io.IOException;

Expand Down Expand Up @@ -105,6 +106,10 @@ public Facets facets() {
return internalResponse.facets();
}

public Suggest suggest() {
return internalResponse.suggest();
}

/**
* The search facets.
*/
Expand Down
Expand Up @@ -127,7 +127,7 @@ protected final void addShardFailure(ShardSearchFailure failure) {

public void start() {
if (scrollId.context().length == 0) {
final InternalSearchResponse internalResponse = new InternalSearchResponse(new InternalSearchHits(InternalSearchHits.EMPTY, Long.parseLong(this.scrollId.attributes().get("total_hits")), 0.0f), null, false);
final InternalSearchResponse internalResponse = new InternalSearchResponse(new InternalSearchHits(InternalSearchHits.EMPTY, Long.parseLong(this.scrollId.attributes().get("total_hits")), 0.0f), null, null, false);
searchCache.releaseQueryFetchResults(queryFetchResults);
listener.onResponse(new SearchResponse(internalResponse, request.scrollId(), 0, 0, 0l, buildShardFailures()));
return;
Expand Down
Expand Up @@ -45,6 +45,7 @@
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestStatus.BAD_REQUEST;
import static org.elasticsearch.rest.action.support.RestXContentBuilder.restContentBuilder;
import static org.elasticsearch.search.suggest.SuggestBuilder.fuzzySuggestion;

/**
*
Expand Down Expand Up @@ -276,6 +277,20 @@ private SearchSourceBuilder parseSearchSource(RestRequest request) {
searchSourceBuilder.stats(Strings.splitStringByCommaToArray(sStats));
}

String suggestField = request.param("suggest_field");
if (suggestField != null) {
String suggestText = request.param("suggest_text", queryString);
int suggestSize = request.paramAsInt("suggest_size", 5);
if (searchSourceBuilder == null) {
searchSourceBuilder = new SearchSourceBuilder();
}
String suggestMode = request.param("suggest_mode");
searchSourceBuilder.suggest().addSuggestion(
fuzzySuggestion(suggestField).setField(suggestField).setText(suggestText).setSize(suggestSize)
.setSuggestMode(suggestMode)
);
}

return searchSourceBuilder;
}
}
Expand Up @@ -41,6 +41,7 @@
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SuggestBuilder;

import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -103,6 +104,8 @@ public static HighlightBuilder highlight() {

private HighlightBuilder highlightBuilder;

private SuggestBuilder suggestBuilder;

private TObjectFloatHashMap<String> indexBoost = null;

private String[] stats;
Expand Down Expand Up @@ -400,6 +403,13 @@ public SearchSourceBuilder highlight(HighlightBuilder highlightBuilder) {
return this;
}

public SuggestBuilder suggest() {
if (suggestBuilder == null) {
suggestBuilder = new SuggestBuilder();
}
return suggestBuilder;
}

/**
* Sets no fields to be loaded, resulting in only id and type to be returned per field.
*/
Expand Down Expand Up @@ -709,6 +719,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
highlightBuilder.toXContent(builder, params);
}

if (suggestBuilder != null) {
suggestBuilder.toXContent(builder, params);
}

if (stats != null) {
builder.startArray("stats");
for (String stat : stats) {
Expand Down
Expand Up @@ -48,6 +48,7 @@
import org.elasticsearch.search.internal.InternalSearchResponse;
import org.elasticsearch.search.query.QuerySearchResult;
import org.elasticsearch.search.query.QuerySearchResultProvider;
import org.elasticsearch.search.suggest.Suggest;

import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -373,7 +374,38 @@ public InternalSearchResponse merge(ShardDoc[] sortedDocs, Map<SearchShardTarget
}
}

// merge suggest results
Suggest suggest = null;
if (!queryResults.isEmpty()) {
List<Suggest.Suggestion> mergedSuggestions = null;
for (QuerySearchResultProvider resultProvider : queryResults.values()) {
Suggest shardResult = resultProvider.queryResult().suggest();
if (shardResult == null) {
continue;
}

if (mergedSuggestions == null) {
mergedSuggestions = shardResult.getSuggestions();
continue;
}

for (Suggest.Suggestion shardCommand : shardResult.getSuggestions()) {
for (Suggest.Suggestion mergedSuggestion : mergedSuggestions) {
if (mergedSuggestion.getName().equals(shardCommand.getName())) {
mergedSuggestion.reduce(shardCommand);
}
}
}
}
if (mergedSuggestions != null) {
suggest = new Suggest(mergedSuggestions);
for (Suggest.Suggestion suggestion : mergedSuggestions) {
suggestion.trim();
}
}
}

InternalSearchHits searchHits = new InternalSearchHits(hits.toArray(new InternalSearchHit[hits.size()]), totalHits, maxScore);
return new InternalSearchResponse(searchHits, facets, timedOut);
return new InternalSearchResponse(searchHits, facets, suggest, timedOut);
}
}
Expand Up @@ -27,6 +27,7 @@
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.facet.Facets;
import org.elasticsearch.search.facet.InternalFacets;
import org.elasticsearch.search.suggest.Suggest;

import java.io.IOException;

Expand All @@ -41,16 +42,19 @@ public class InternalSearchResponse implements Streamable, ToXContent {

private InternalFacets facets;

private Suggest suggest;

private boolean timedOut;

public static final InternalSearchResponse EMPTY = new InternalSearchResponse(new InternalSearchHits(new InternalSearchHit[0], 0, 0), null, false);
public static final InternalSearchResponse EMPTY = new InternalSearchResponse(new InternalSearchHits(new InternalSearchHit[0], 0, 0), null, null, false);

private InternalSearchResponse() {
}

public InternalSearchResponse(InternalSearchHits hits, InternalFacets facets, boolean timedOut) {
public InternalSearchResponse(InternalSearchHits hits, InternalFacets facets, Suggest suggest, boolean timedOut) {
this.hits = hits;
this.facets = facets;
this.suggest = suggest;
this.timedOut = timedOut;
}

Expand All @@ -66,12 +70,19 @@ public Facets facets() {
return facets;
}

public Suggest suggest() {
return suggest;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
hits.toXContent(builder, params);
if (facets != null) {
facets.toXContent(builder, params);
}
if (suggest != null) {
suggest.toXContent(builder, params);
}
return builder;
}

Expand All @@ -87,6 +98,9 @@ public void readFrom(StreamInput in) throws IOException {
if (in.readBoolean()) {
facets = InternalFacets.readFacets(in);
}
if (in.readBoolean()) {
suggest = Suggest.readSuggest(in);
}
timedOut = in.readBoolean();
}

Expand All @@ -99,6 +113,12 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(true);
facets.writeTo(out);
}
if (suggest == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
suggest.writeTo(out);
}
out.writeBoolean(timedOut);
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/elasticsearch/search/internal/SearchContext.java
Expand Up @@ -60,6 +60,7 @@
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.search.query.QuerySearchResult;
import org.elasticsearch.search.scan.ScanContext;
import org.elasticsearch.search.suggest.SuggestionSearchContext;

import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -160,6 +161,8 @@ public static SearchContext current() {

private SearchContextHighlight highlight;

private SuggestionSearchContext suggest;

private SearchLookup searchLookup;

private boolean queryRewritten;
Expand Down Expand Up @@ -301,6 +304,14 @@ public void highlight(SearchContextHighlight highlight) {
this.highlight = highlight;
}

public SuggestionSearchContext suggest() {
return suggest;
}

public void suggest(SuggestionSearchContext suggest) {
this.suggest = suggest;
}

public boolean hasScriptFields() {
return scriptFields != null;
}
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/org/elasticsearch/search/query/QueryPhase.java
Expand Up @@ -35,6 +35,7 @@
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.search.sort.SortParseElement;
import org.elasticsearch.search.sort.TrackScoresParseElement;
import org.elasticsearch.search.suggest.SuggestPhase;

import java.util.Map;

Expand All @@ -44,10 +45,12 @@
public class QueryPhase implements SearchPhase {

private final FacetPhase facetPhase;
private final SuggestPhase suggestPhase;

@Inject
public QueryPhase(FacetPhase facetPhase) {
public QueryPhase(FacetPhase facetPhase, SuggestPhase suggestPhase) {
this.facetPhase = facetPhase;
this.suggestPhase = suggestPhase;
}

@Override
Expand All @@ -68,7 +71,8 @@ public QueryPhase(FacetPhase facetPhase) {
.put("min_score", new MinScoreParseElement())
.put("minScore", new MinScoreParseElement())
.put("timeout", new TimeoutParseElement())
.putAll(facetPhase.parseElements());
.putAll(facetPhase.parseElements())
.putAll(suggestPhase.parseElements());
return parseElements.build();
}

Expand Down Expand Up @@ -185,6 +189,7 @@ public void execute(SearchContext searchContext) throws QueryPhaseExecutionExcep
searchContext.searcher().processedScope();
}

suggestPhase.execute(searchContext);
facetPhase.execute(searchContext);
}
}
Expand Up @@ -25,6 +25,7 @@
import org.elasticsearch.search.SearchShardTarget;
import org.elasticsearch.search.facet.Facets;
import org.elasticsearch.search.facet.InternalFacets;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.transport.TransportResponse;

import java.io.IOException;
Expand All @@ -43,6 +44,7 @@ public class QuerySearchResult extends TransportResponse implements QuerySearchR
private int size;
private TopDocs topDocs;
private InternalFacets facets;
private Suggest suggest;
private boolean searchTimedOut;

public QuerySearchResult() {
Expand Down Expand Up @@ -101,6 +103,14 @@ public void facets(InternalFacets facets) {
this.facets = facets;
}

public Suggest suggest() {
return suggest;
}

public void suggest(Suggest suggest) {
this.suggest = suggest;
}

public int from() {
return from;
}
Expand Down Expand Up @@ -136,6 +146,9 @@ public void readFrom(StreamInput in) throws IOException {
if (in.readBoolean()) {
facets = InternalFacets.readFacets(in);
}
if (in.readBoolean()) {
suggest = Suggest.readSuggest(in);
}
searchTimedOut = in.readBoolean();
}

Expand All @@ -153,6 +166,12 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(true);
facets.writeTo(out);
}
if (suggest == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
suggest.writeTo(out);
}
out.writeBoolean(searchTimedOut);
}
}

0 comments on commit 32e4a58

Please sign in to comment.