Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/111834.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 111834
summary: Add inner hits support to semantic query
area: Search
type: enhancement
issues: []
216 changes: 208 additions & 8 deletions docs/reference/query-dsl/semantic-query.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ GET my-index-000001/_search
}
}
------------------------------------------------------------
// TEST[skip:TBD]
// TEST[skip: Requires inference endpoints]


[discrete]
Expand All @@ -40,9 +40,209 @@ The `semantic_text` field to perform the query on.
(Required, string)
The query text to be searched for on the field.

`inner_hits`::
(Optional, object)
Retrieves the specific passages that match the query.
See <<semantic-query-passage-ranking, passage ranking with the `semantic` query>> for more information.
+
.Properties of `inner_hits`
[%collapsible%open]
====
`from`::
(Optional, integer)
The offset from the first matching passage to fetch.
Used to paginate through the passages.
Defaults to `0`.

`size`::
(Optional, integer)
The maximum number of matching passages to return.
Defaults to `3`.
====

Refer to <<semantic-search-semantic-text,this tutorial>> to learn more about semantic search using `semantic_text` and `semantic` query.

[discrete]
[[semantic-query-passage-ranking]]
==== Passage ranking with the `semantic` query
The `inner_hits` parameter can be used for _passage ranking_, which allows you to determine which passages in the document best match the query.
For example, if you have a document that covers varying topics:

[source,console]
------------------------------------------------------------
POST my-index/_doc/lake_tahoe
{
"inference_field": [
"Lake Tahoe is the largest alpine lake in North America",
"When hiking in the area, please be on alert for bears"
]
}
------------------------------------------------------------
// TEST[skip: Requires inference endpoints]

You can use passage ranking to find the passage that best matches your query:

[source,console]
------------------------------------------------------------
GET my-index/_search
{
"query": {
"semantic": {
"field": "inference_field",
"query": "mountain lake",
"inner_hits": { }
}
}
}
------------------------------------------------------------
// TEST[skip: Requires inference endpoints]

[source,console-result]
------------------------------------------------------------
{
"took": 67,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 10.844536,
"hits": [
{
"_index": "my-index",
"_id": "lake_tahoe",
"_score": 10.844536,
"_source": {
...
},
"inner_hits": { <1>
"inference_field": {
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 10.844536,
"hits": [
{
"_index": "my-index",
"_id": "lake_tahoe",
"_nested": {
"field": "inference_field.inference.chunks",
"offset": 0
},
"_score": 10.844536,
"_source": {
"text": "Lake Tahoe is the largest alpine lake in North America"
}
},
{
"_index": "my-index",
"_id": "lake_tahoe",
"_nested": {
"field": "inference_field.inference.chunks",
"offset": 1
},
"_score": 3.2726858,
"_source": {
"text": "When hiking in the area, please be on alert for bears"
}
}
]
}
}
}
}
]
}
}
------------------------------------------------------------
<1> Ranked passages will be returned using the <<inner-hits,`inner_hits` response format>>, with `<inner_hits_name>` set to the `semantic_text` field name.

By default, the top three matching passages will be returned.
You can use the `size` parameter to control the number of passages returned and the `from` parameter to page through the matching passages:

[source,console]
------------------------------------------------------------
GET my-index/_search
{
"query": {
"semantic": {
"field": "inference_field",
"query": "mountain lake",
"inner_hits": {
"from": 1,
"size": 1
}
}
}
}
------------------------------------------------------------
// TEST[skip: Requires inference endpoints]

[source,console-result]
------------------------------------------------------------
{
"took": 42,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 10.844536,
"hits": [
{
"_index": "my-index",
"_id": "lake_tahoe",
"_score": 10.844536,
"_source": {
...
},
"inner_hits": {
"inference_field": {
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 10.844536,
"hits": [
{
"_index": "my-index",
"_id": "lake_tahoe",
"_nested": {
"field": "inference_field.inference.chunks",
"offset": 1
},
"_score": 3.2726858,
"_source": {
"text": "When hiking in the area, please be on alert for bears"
}
}
]
}
}
}
}
]
}
}
------------------------------------------------------------

[discrete]
[[hybrid-search-semantic]]
==== Hybrid search with the `semantic` query
Expand Down Expand Up @@ -79,7 +279,7 @@ POST my-index/_search
}
}
------------------------------------------------------------
// TEST[skip:TBD]
// TEST[skip: Requires inference endpoints]

You can also use semantic_text as part of <<rrf,Reciprocal Rank Fusion>> to make ranking relevant results easier:

Expand Down Expand Up @@ -116,12 +316,12 @@ GET my-index/_search
}
}
------------------------------------------------------------
// TEST[skip:TBD]
// TEST[skip: Requires inference endpoints]


[discrete]
[[advanced-search]]
=== Advanced search on `semantic_text` fields
==== Advanced search on `semantic_text` fields

The `semantic` query uses default settings for searching on `semantic_text` fields for ease of use.
If you want to fine-tune a search on a `semantic_text` field, you need to know the task type used by the `inference_id` configured in `semantic_text`.
Expand All @@ -135,7 +335,7 @@ on a `semantic_text` field, it is not supported to use the `semantic_query` on a

[discrete]
[[search-sparse-inference]]
==== Search with `sparse_embedding` inference
===== Search with `sparse_embedding` inference

When the {infer} endpoint uses a `sparse_embedding` model, you can use a <<query-dsl-sparse-vector-query,`sparse_vector` query>> on a <<semantic-text,`semantic_text`>> field in the following way:

Expand All @@ -157,14 +357,14 @@ GET test-index/_search
}
}
------------------------------------------------------------
// TEST[skip:TBD]
// TEST[skip: Requires inference endpoints]

You can customize the `sparse_vector` query to include specific settings, like <<sparse-vector-query-with-pruning-config-and-rescore-example,pruning configuration>>.


[discrete]
[[search-text-inferece]]
==== Search with `text_embedding` inference
===== Search with `text_embedding` inference

When the {infer} endpoint uses a `text_embedding` model, you can use a <<query-dsl-knn-query,`knn` query>> on a `semantic_text` field in the following way:

Expand All @@ -190,6 +390,6 @@ GET test-index/_search
}
}
------------------------------------------------------------
// TEST[skip:TBD]
// TEST[skip: Requires inference endpoints]

You can customize the `knn` query to include specific settings, like `num_candidates` and `k`.
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ static TransportVersion def(int id) {
public static final TransportVersion ILM_ADD_SEARCHABLE_SNAPSHOT_TOTAL_SHARDS_PER_NODE = def(8_749_00_0);
public static final TransportVersion SEMANTIC_TEXT_SEARCH_INFERENCE_ID = def(8_750_00_0);
public static final TransportVersion ML_INFERENCE_CHUNKING_SETTINGS = def(8_751_00_0);
public static final TransportVersion SEMANTIC_QUERY_INNER_HITS = def(8_752_00_0);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ public final class InnerHitBuilder implements Writeable, ToXContentObject {
public static final ParseField COLLAPSE_FIELD = new ParseField("collapse");
public static final ParseField FIELD_FIELD = new ParseField("field");

public static final int DEFAULT_FROM = 0;
public static final int DEFAULT_SIZE = 3;
private static final boolean DEFAULT_IGNORE_UNAMPPED = false;
private static final int DEFAULT_FROM = 0;
private static final int DEFAULT_SIZE = 3;
private static final boolean DEFAULT_VERSION = false;
private static final boolean DEFAULT_SEQ_NO_AND_PRIMARY_TERM = false;
private static final boolean DEFAULT_EXPLAIN = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.features.FeatureSpecification;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper;
import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder;
import org.elasticsearch.xpack.inference.rank.random.RandomRankRetrieverBuilder;
import org.elasticsearch.xpack.inference.rank.textsimilarity.TextSimilarityRankRetrieverBuilder;

Expand All @@ -25,7 +26,8 @@ public Set<NodeFeature> getFeatures() {
return Set.of(
TextSimilarityRankRetrieverBuilder.TEXT_SIMILARITY_RERANKER_RETRIEVER_SUPPORTED,
RandomRankRetrieverBuilder.RANDOM_RERANKER_RETRIEVER_SUPPORTED,
SemanticTextFieldMapper.SEMANTIC_TEXT_SEARCH_INFERENCE_ID
SemanticTextFieldMapper.SEMANTIC_TEXT_SEARCH_INFERENCE_ID,
SemanticQueryBuilder.SEMANTIC_TEXT_INNER_HITS
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.elasticsearch.index.mapper.ValueFetcher;
import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper;
import org.elasticsearch.index.mapper.vectors.SparseVectorFieldMapper;
import org.elasticsearch.index.query.InnerHitBuilder;
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
Expand All @@ -54,6 +55,7 @@
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xpack.core.ml.inference.results.MlTextEmbeddingResults;
import org.elasticsearch.xpack.core.ml.inference.results.TextExpansionResults;
import org.elasticsearch.xpack.inference.queries.SemanticQueryInnerHitBuilder;

import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -468,7 +470,12 @@ public boolean fieldHasValue(FieldInfos fieldInfos) {
return fieldInfos.fieldInfo(getEmbeddingsFieldName(name())) != null;
}

public QueryBuilder semanticQuery(InferenceResults inferenceResults, float boost, String queryName) {
public QueryBuilder semanticQuery(
InferenceResults inferenceResults,
float boost,
String queryName,
SemanticQueryInnerHitBuilder semanticInnerHitBuilder
) {
String nestedFieldPath = getChunksFieldName(name());
String inferenceResultsFieldName = getEmbeddingsFieldName(name());
QueryBuilder childQueryBuilder;
Expand Down Expand Up @@ -524,7 +531,10 @@ public QueryBuilder semanticQuery(InferenceResults inferenceResults, float boost
};
}

return new NestedQueryBuilder(nestedFieldPath, childQueryBuilder, ScoreMode.Max).boost(boost).queryName(queryName);
InnerHitBuilder innerHitBuilder = semanticInnerHitBuilder != null ? semanticInnerHitBuilder.toInnerHitBuilder() : null;
return new NestedQueryBuilder(nestedFieldPath, childQueryBuilder, ScoreMode.Max).boost(boost)
.queryName(queryName)
.innerHit(innerHitBuilder);
}

private String generateQueryInferenceResultsTypeMismatchMessage(InferenceResults inferenceResults, String expectedResultsType) {
Expand Down
Loading
Loading