Skip to content

Conversation

ioanatia
Copy link
Contributor

@ioanatia ioanatia commented Nov 5, 2024

tracked in: #115103

This brings the ability to execute semantic queries using the match function on semantic_text fields.

How to test:

  1. Start a trial - the inference API is not available in basic: POST /_license/start_trial?acknowledge=true.
  2. Create an index using a semantic_text field, here we are using the default ELSER endpoint:
PUT testidx { "mappings": { "properties": { "semantic_text": { "type": "semantic_text", "inference_id": ".elser-2-elasticsearch" } } } } 
  1. Ingest some data: this might take a while if ELSER is not yet deployed. To check the status it's useful to have Kibana running, and under Machine Learning > Trained Models you can check the status of the ELSER model:
POST testidx/_doc { "semantic_text": "live long and prosper" } 
  1. Query in ES|QL:
POST _query?format=txt { "query": """ FROM testidx | WHERE match(semantic_text, "live") """ } 

Execution

What we do in _search:

GET testidx/_search { "query": { "bool": { "should": { "semantic": { "field": "semantic_text", "query": "live long" } } } } } 

In _search when we execute a semantic query, we have 2 rewrite phases:

  1. We do an inference call where we send the query string live long - the inference results are the embeddings we will use to querying the semantic_text field. Retrieving the embeddings is done on the coordinator, because we do not want to do an inference call on every shard. The inference endpoint might be configured to work with an external, paid service like Azure OpenAI/Amazon Bedrock.
  2. On the data node, we do another rewrite depending on the field type- e.g. if the field is configured to work with dense vectors, we rewrite to an exact KNN query.

For ES|QL we kept the same execution flow:

  1. On the coordinator, after the logical plan has been analyzed and verified, we look for any match function that has a semantic_text argument. We collect these semantic query arguments and call the inference service to retrieve the embeddings. We then embed the inference results in the Match objects that represent semantic queries(InferenceUtils).
  2. When we translate the Match function to a query builder as part of pushing the filters to Lucene, we do the same type of query rewriting that is done in _search depending on whether the inference results are dense or sparse vectors.I I initially wanted to use SemanticQueryBuilder but that resulted in a dependency hell. NOTE: this is only temporary, very soon we should have support for semantic_text in MatchQueryBuilder, so this logic will be simplified.

For licensing:

I did not add any extra licensing checks. After consulting with team, it seems those are not needed.
SemanticQueryBuilder also does not do any licensing checks. It relies on the inference APIs to have proper licensing handling.

Checklist:

  • CCS is not supported - it's not supported for semantic queries in _search either - but we need to check we don't fail with a funny error, but with a meaningful message
  • it is not possible to query a semantic_text field that has different inference_ids - validations/tests are needed, this is not supported in _search too
@ioanatia ioanatia added >non-issue auto-backport Automatically create backport pull requests when merged :Analytics/ES|QL AKA ESQL v9.0.0 v8.17.0 labels Nov 5, 2024
name 'x-pack-esql'
description 'The plugin that powers ESQL for Elasticsearch'
classname 'org.elasticsearch.xpack.esql.plugin.EsqlPlugin'
extendedPlugins = ['x-pack-esql-core', 'lang-painless', 'x-pack-ml']
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed to change build.gradle for both esql-core and esql because we need SemanticQueryBuilder.
I had to remove x-pack-ml because of jar hell.
The problem is that the categorize integration tests no longer work:

REPRODUCE WITH: ./gradlew ":x-pack:plugin:esql:qa:server:single-node:javaRestTest" --tests "org.elasticsearch.xpack.esql.qa.single_node.EsqlSpecIT" -Dtests.method="test {categorize.Categorize ASYNC}" [2024-11-18T19:19:08,448][ERROR][o.e.b.ElasticsearchUncaughtExceptionHandler] [test-cluster-0] fatal error in thread [elasticsearch[test-cluster-0][search][T#47]], exiting java.lang.NoClassDefFoundError: org/elasticsearch/xpack/ml/job/categorization/CategorizationAnalyzer |   -- | --   | at org.elasticsearch.xpack.esql.expression.function.grouping.Categorize.toEvaluator(Categorize.java:112) |     | at org.elasticsearch.xpack.esql.evaluator.EvalMapper.toEvaluator(EvalMapper.java:55) |     | at org.elasticsearch.xpack.esql.planner.LocalExecutionPlanner.planEval(LocalExecutionPlanner.java:410) |     | at org.elasticsearch.xpack.esql.planner.LocalExecutionPlanner.plan(LocalExecutionPlanner.java:197) |     | at org.elasticsearch.xpack.esql.planner.LocalExecutionPlanner.planAggregation(LocalExecutionPlanner.java:242) |     | at org.elasticsearch.xpack.esql.planner.LocalExecutionPlanner.plan(LocalExecutionPlanner.java:189) ... Caused by: java.lang.ClassNotFoundException: org.elasticsearch.xpack.ml.job.categorization.CategorizationAnalyzer |   -- | --   | at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445) |     | at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:595) |     | at java.base/java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:870) |     | at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:528) 

I don't know how to fix this - if I leave x-pack-ml here, we get Jar hell when Elasticsearch starts:

at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:76) Caused by: java.lang.IllegalStateException: jar hell! class: com.ibm.icu.impl.Assert jar1: /Users/ioana/projects/elasticsearch/distribution/archives/darwin-tar/build/install/elasticsearch-9.0.0-SNAPSHOT/modules/x-pack-ml/icu4j-68.2.jar jar2: /Users/ioana/projects/elasticsearch/distribution/archives/darwin-tar/build/install/elasticsearch-9.0.0-SNAPSHOT/modules/x-pack-esql-core/icu4j-68.2.jar	at org.elasticsearch.base@9.0.0-SNAPSHOT/org.elasticsearch.jdk.JarHell.checkClass(JarHell.java:316)	at org.elasticsearch.base@9.0.0-SNAPSHOT/org.elasticsearch.jdk.JarHell.checkJarHell(JarHell.java:234)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.plugins.PluginsUtils.checkBundleJarHell(PluginsUtils.java:319)	... 9 more 

inference and ml seem to have many common dependencies and AFAIK esql is the only plugin that seems to need both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved this by no longer using SemanticQueryBuilder - this means we no longer need to extend from x-pack-inference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to make both search and ml work here, we should figure out how to deal with the dependency other than deferring it. @jan-elastic @ChrisHegarty do you have any idea whether it is ok to remove x-pack-ml here, or how to make it work?

Copy link
Contributor Author

@ioanatia ioanatia Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is ok to remove x-pack-ml here, or how to make it work?

It's okay to remove it because we added it to x-pack-esql-core - x-pack-esql-core now extends x-pack-ml and x-pack-esql extends x-pack-esql-core.
When I simply removed x-pack-ml and replaced it x-pack-inference, the CATEGORIZE tests would start to fail. So I know this should be safe, because I can see CATEGORIZE works as expected (none of the tests for it fail).

we should figure out how to deal with the dependency other than deferring it.

I agree - I went back and forth on this and this is the best we could come up with:

  • we could move SemanticQueryBuilder in x-pack-core - this would mean moving some additional classes like SemanticTextFieldMapper, maybe more, I haven't unravelled this one.
  • however even if we can use SemanticQueryBuilder in ES|QL this would only be temporary - the reason for this is that for _search we want the match query to actually support semantic_text (Match query support for semantic_text fields - POC DO NOT MERGE #112166 - a POC and the actual implementation will be done in the following weeks).
  • if you take a look at the match query POC it looks like MatchQueryBuilder has semantic_text support - MatchQueryBuilder is in server so we should be able to use without dependency issues (we already use it).

So my take is that moving SemanticQueryBuilder does not gain us much if support in MatchQueryBuilder is coming soon. And I think that once that's coming, we might be able to revert build.gradle to what it was originally.
I captured that in a bullet point in #115103

Copy link
Member

@carlosdelest carlosdelest left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM - great work! 💯

We should get eyes from the AE team

}

private String embeddingsFieldName() {
return name.concat(".inference.chunks.embeddings");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a pity we can't access this constant from the ML plugin 😓

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What prevents us from accessing this constant from the ML plugin? Is it related to the dependency issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not from the ML plugin - they are from the inference plugin:

static final String TEXT_FIELD = "text";
static final String INFERENCE_FIELD = "inference";
static final String INFERENCE_ID_FIELD = "inference_id";
static final String SEARCH_INFERENCE_ID_FIELD = "search_inference_id";
static final String CHUNKS_FIELD = "chunks";
static final String CHUNKED_EMBEDDINGS_FIELD = "embeddings";
static final String CHUNKED_TEXT_FIELD = "text";
static final String MODEL_SETTINGS_FIELD = "model_settings";
static final String TASK_TYPE_FIELD = "task_type";
static final String DIMENSIONS_FIELD = "dimensions";
static final String SIMILARITY_FIELD = "similarity";
static final String ELEMENT_TYPE_FIELD = "element_type";

We can import things from the ML plugin, but once we extend the inference plugin we end up with jar hell.

@ioanatia ioanatia requested a review from afoucret November 21, 2024 16:04
@ioanatia ioanatia mentioned this pull request Nov 21, 2024
16 tasks
@ioanatia ioanatia added the test-release Trigger CI checks against release build label Nov 22, 2024
- match: { columns: [{name: name, type: keyword}] }

---
semantic_text declared in mapping:
Copy link
Contributor Author

@ioanatia ioanatia Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this test - it failed for serverless tests on mixed version clusters with Invalid EsField type [SemanticTextEsField]. However:

  • this test is a kind of a hack - it puts the full internal representation for the semantic_text field which will change, so this test will need change to use the test inference plugin so that the embeddings are internally generated - but then we have the same problem as Add mixed mode BWC testing support for test-specific plugins #115166

  • the amount of CSV tests we have already cover the fact that for ESQL the block loader is capable of only returning the original text content

  • semantic_text is only available in snapshots - so not available for serverless - the fact that this test fails has no meaning for serverless prod

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a yml test for semantic_text is better, if serverless is affected, can this be muted on serverless? Better keep it for stateful if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted the removal and muted the test on serverless.
Also keeping track in #115103 to not forget to unmute this test in serverless once the changes here make it in 9.0.0 snapshot

Copy link
Member

@fang-xing-esql fang-xing-esql left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting these together @ioanatia ! I added some comments, and made a summary of my thoughts so far:

  • This is a much more complicated feature compared to KQL, as it touches a lot of more areas, and it worth spending more time on it.
  • I was looking for design docs, however I have no luck finding one that help me clarify the questions in my mind yet. There are a few areas that I think they worth further discussions:
    1. The workflow of inference resolution in EsqlSession, I wonder if any alternative approaches were being considered or discussed. Have we considered implementing inference resolution in a similar way as index or enrich policy resolver? Making Inference resolution independent of them seems more clean if possible, so that we can minimize unnecessary interface changes or changes to tests that are irrelevant to inference resolution.
    2. What is the best way to tell Analyzer that there is remote cluster involved? And how to deal with it within the scope of match?
    3. How to deal with semantic_text fields with multiple inference ids/results(this is allowed in the SemanticTextEsField definition), is it necessary to deal with them when there is no match function/operator in the query? Resolving inference adds extra cost, it is better to do it when we know that we will need it. It is likely multiple inference ids on the same semantic text field happens when there are multiple indices that match a common index pattern in this case, to make it more complicated, the data type could be different given the same field name, do we know how they behave?
  • More negative and positive tests will be appreciated, as they are the only way to exercise all possible code paths.
name 'x-pack-esql'
description 'The plugin that powers ESQL for Elasticsearch'
classname 'org.elasticsearch.xpack.esql.plugin.EsqlPlugin'
extendedPlugins = ['x-pack-esql-core', 'lang-painless', 'x-pack-ml']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to make both search and ml work here, we should figure out how to deal with the dependency other than deferring it. @jan-elastic @ChrisHegarty do you have any idea whether it is ok to remove x-pack-ml here, or how to make it work?

FieldAttribute field = (FieldAttribute) match.field();
SemanticTextEsField esField = (SemanticTextEsField) field.field();
if (esField.inferenceIds().size() == 1) {
result.add(new SemanticQuery(field.sourceText(), match.query().sourceText(), esField.inferenceIds().iterator().next()));
Copy link
Member

@fang-xing-esql fang-xing-esql Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first inferenceId is used here, match works with one inferenceId only, however multiple inferenceId can be defined in SemanticTextEsField. it will be appreciated if there are some comments here, and if we don't deal with multiple inference ids, does it make sense to keep just one inference id in SemanticTextEsField?

In case there are multiple inferenceIds, do we want to throw an exception here? Similar with what is done in Verifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case there are multiple inferenceIds, do we want to throw an exception here? Similar with what is done in Verifier.

I guess it does not hurt to throw an exception - but at this point in the execution, the Verifier already checked and returned an error if we are dealing with multiple inference ids.

} else {

// This should never happen, but we handle it here either way
throw new IllegalStateException("Cannot handle inference results");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is IllegalStateException a 400 or 500?

Comment on lines 308 to 324
public Set<String> inferenceIdsForField(
Map<String, IndexMetadata> indexMetadata,
FieldCapabilitiesResponse fieldCapsResponse,
String name
) {
Set<String> inferenceIds = new HashSet<>();

for (FieldCapabilitiesIndexResponse ir : fieldCapsResponse.getIndexResponses()) {
String indexName = ir.getIndexName();
InferenceFieldMetadata inferenceFieldMetadata = indexMetadata.get(indexName).getInferenceFields().get(name);
if (inferenceFieldMetadata != null) {
inferenceIds.add(inferenceFieldMetadata.getInferenceId());
}
}
return inferenceIds;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider decoupling this from IndexResolver and make an InferenceResolver to handle inference related tasks.

10043 | Yishay | Tzvieli
;

testMatchWithSemanticText
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More positive tests will be appreciated. How does it work with multi-valued fields? More combinations(conjunction) with other functions and operators, before/after stats/eval when it works, for the completeness. There could be surprises, without testing it out, it is hard to tell.

8 | William Faulkner
;

testMatchWithSemanticText
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as match-function, more variety of queries will be appreciated.

);
}

public void testMatchWithSemanticTextWithCCQ() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More negative tests will be appreciated, we have code to detect errors, without having negative tests to cover those code paths, it is hard to tell whether they work as expected. Here are a few scenarios that I can think of:

  • Match on semantic_text fields with multiple inference_ids
  • Match on multiple indices that have a common index pattern, the field names is the same, however they may have different data types - text, keyword, semantic_text
  • QSTR or KQL with semantic_text field in the index mapping, they are search functions as well.
  • Include semantic_text in MatchTests and MatchOperatorTests.
- match: { columns: [{name: name, type: keyword}] }

---
semantic_text declared in mapping:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a yml test for semantic_text is better, if serverless is affected, can this be muted on serverless? Better keep it for stateful if possible.

@ioanatia
Copy link
Contributor Author

Thank you @fang-xing-esql for your review and thanks for meeting with me to discuss this.
A few notes:

  • I added an InferenceResolver as we discussed - this means we no longer need to pass client or clusterService all the way down to EsqlSession.
  • I added a boolean isCrossClusterSearch flag in Configuration - this made it possible to only to contain the check to return an error when semantic search is used with CCS in the Match function - the footprint of changing the Configuration constructor is still bigger than I hoped.
  • I haven't reverted the changes back in IndexResolver but that is coming.

Looking at ways to simplify this I am thinking to:

  • have the InferenceResolver be responsible for the two checks we want to add which are similar to what we have in _search:
    • cannot use match with semantic_text fields when doing CCS
    • cannot use match with a semantic_text field if it has more than one inference IDs in the queried indices

If the InferenceResolver is responsible for these checks:

  • the match function does not need to do these checks anymore, so we no longer need to modify Configuration
  • we no longer need SemanticTextEsField - we can as we did before just use EsField for semantic_text fields. SemanticTextEsField was created because we needed a way to store the inferenceIds for them to be checked later on in the Match function (or the Verifier as it was initially) and on the actual inference result retrieval. If the multiple inference IDs check is done in InferenceResolver the concept of mapped inference IDs is contained solely in the InferenceResolver.
    • If we remove SemanticTextEsField we no longer need to mute the bwc serverless test that was complaining that it could deserialize SemanticTextEsField with Invalid EsField type [SemanticTextEsField]
    • I can revert back all the modifications done to IndexResolver

I think I also need to add a comment in EsqlSession on why we get the inference results after the Analyzer and Verifier. The thinking was:

  • the inference calls take time to execute - the inference API can call on its own an external service Amazon Bedrock, Azure Open AI etc. So we want to defer getting the inference results after we have assurance that the query is valid.
  • the Analyzer resolves the functions from UnresolvedFunction and also resolves the fields that are being used - we would need that to detect which Match functions use semantic_text fields

Will follow up on this tomorrow. thanks!

@fang-xing-esql
Copy link
Member

I started to do some experiments with the branch, and came across an issue when we have multiple indices with the same semantic_text field name, but different inference ids, here are the steps to reproduce it.

curl -u elastic:password -X POST "localhost:9200/_license/start_trial?acknowledge=true" curl -u elastic:password -X DELETE "localhost:9200/testidx*/" curl -u elastic:password -X PUT "localhost:9200/testidx00?pretty" -H 'Content-Type: application/json' -d' { "mappings": { "properties": { "textfield": { "type": "semantic_text", "inference_id": ".elser-2-elasticsearch" } } } } ' curl -u elastic:password -X PUT "localhost:9200/testidx01?pretty" -H 'Content-Type: application/json' -d' { "mappings": { "properties": { "textfield": { "type": "semantic_text", "inference_id": ".multilingual-e5-small-elasticsearch" } } } } ' curl -X PUT "localhost:9200/testidx00/_bulk?refresh&pretty" -H 'Content-Type: application/json' -d' {"index": {}} {"textfield": "live long and prosper 00 elser"} ' curl -X PUT "localhost:9200/testidx01/_bulk?refresh&pretty" -H 'Content-Type: application/json' -d' {"index": {}} {"textfield": "live long and prosper 01 multilingual"} ' curl -u elastic:password -v -X POST "localhost:9200/_query?format=txt&pretty" -H 'Content-Type: application/json' -d' { "query": "from testidx00, testidx01 | where match(textfield, \"live\")" } ' [2024-12-02T14:58:03,147][ERROR][o.e.b.ElasticsearchUncaughtExceptionHandler] [runTask-0] fatal error in thread [elasticsearch[runTask-0][search_coordination][T#2]], exiting java.lang.AssertionError: Should never have a semantic_text field with no inference ID attached	at org.elasticsearch.xpack.esql.session.InferenceResolver.lambda$semanticQueries$1(InferenceResolver.java:155)	at org.elasticsearch.xpack.esql.core.tree.Node.lambda$forEachDown$1(Node.java:77)	at org.elasticsearch.xpack.esql.core.tree.Node.forEachDown(Node.java:69)	at org.elasticsearch.xpack.esql.core.tree.Node.forEachDown(Node.java:75)	at org.elasticsearch.xpack.esql.plan.QueryPlan.lambda$forEachExpressionDown$13(QueryPlan.java:176)	at org.elasticsearch.xpack.esql.plan.QueryPlan.doForEachExpression(QueryPlan.java:190)	at org.elasticsearch.xpack.esql.plan.QueryPlan.lambda$forEachExpressionDown$14(QueryPlan.java:176)	at org.elasticsearch.xpack.esql.core.tree.Node.forEachProperty(Node.java:114)	at org.elasticsearch.xpack.esql.core.tree.Node.lambda$forEachPropertyDown$4(Node.java:102)	at org.elasticsearch.xpack.esql.core.tree.Node.forEachDown(Node.java:69)	at org.elasticsearch.xpack.esql.core.tree.Node.lambda$forEachDown$0(Node.java:70)	at java.base/java.util.Collections$SingletonList.forEach(Collections.java:5188)	at org.elasticsearch.xpack.esql.core.tree.Node.forEachDown(Node.java:70)	at org.elasticsearch.xpack.esql.core.tree.Node.forEachPropertyDown(Node.java:102)	at org.elasticsearch.xpack.esql.plan.QueryPlan.forEachExpressionDown(QueryPlan.java:176)	at org.elasticsearch.xpack.esql.session.InferenceResolver.semanticQueries(InferenceResolver.java:137)	at org.elasticsearch.xpack.esql.session.InferenceResolver.setInferenceResults(InferenceResolver.java:68)	at org.elasticsearch.xpack.esql.session.EsqlSession$1.onResponse(EsqlSession.java:161)	at org.elasticsearch.xpack.esql.session.EsqlSession$1.onResponse(EsqlSession.java:157)	at org.elasticsearch.xpack.esql.session.EsqlSession.lambda$preAnalyze$13(EsqlSession.java:372)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$ResponseWrappingActionListener.onResponse(ActionListenerImplementations.java:247)	at org.elasticsearch.xpack.esql.session.IndexResolver.lambda$resolveAsMergedMapping$0(IndexResolver.java:83)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$ResponseWrappingActionListener.onResponse(ActionListenerImplementations.java:247)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.ActionListener$3.onResponse(ActionListener.java:400)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.tasks.TaskManager$1.onResponse(TaskManager.java:203)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.tasks.TaskManager$1.onResponse(TaskManager.java:197)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$RunBeforeActionListener.onResponse(ActionListenerImplementations.java:336)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.ActionListener$3.onResponse(ActionListener.java:400)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.fieldcaps.TransportFieldCapabilitiesAction.mergeIndexResponses(TransportFieldCapabilitiesAction.java:339)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.fieldcaps.TransportFieldCapabilitiesAction.lambda$doExecuteForked$7(TransportFieldCapabilitiesAction.java:236)	at org.elasticsearch.base@9.0.0-SNAPSHOT/org.elasticsearch.core.AbstractRefCounted$1.closeInternal(AbstractRefCounted.java:125)	at org.elasticsearch.base@9.0.0-SNAPSHOT/org.elasticsearch.core.AbstractRefCounted.decRef(AbstractRefCounted.java:77)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.support.RefCountingRunnable.close(RefCountingRunnable.java:113)	at org.elasticsearch.base@9.0.0-SNAPSHOT/org.elasticsearch.core.Releasables$4.close(Releasables.java:161)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.RunOnce.run(RunOnce.java:41)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.fieldcaps.RequestDispatcher.innerExecute(RequestDispatcher.java:159)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.action.fieldcaps.RequestDispatcher$1.doRun(RequestDispatcher.java:128)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:27)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.TimedRunnable.doRun(TimedRunnable.java:34)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:1023)	at org.elasticsearch.server@9.0.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:27)	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)	at java.base/java.lang.Thread.run(Thread.java:1575) 
Copy link
Member

@fang-xing-esql fang-xing-esql left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing the issue with multiple inference ids @ioanatia! I got chance to try more today and add some comments with my findings.

Set<String> indexNames = indexNames(analyzedPlan);

analyzedPlan.forEachExpressionDown(Match.class, match -> {
if (match.field().dataType() == DataType.SEMANTIC_TEXT && match.field() instanceof FieldAttribute field) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this case if it hasn't been covered yet? Schema is here.

+ curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx00 | eval x = textfield | where match(x, \"live\")" } ' { "error" : { "root_cause" : [ { "type" : "null_pointer_exception", "reason" : "Cannot invoke \"org.elasticsearch.inference.InferenceResults.getClass()\" because \"this.inferenceResults\" is null" } ], "type" : "null_pointer_exception", "reason" : "Cannot invoke \"org.elasticsearch.inference.InferenceResults.getClass()\" because \"this.inferenceResults\" is null", "suppressed" : [ { "type" : "exception", "reason" : "1 further exceptions were dropped" }, { "type" : "task_cancelled_exception", "reason" : "parent task was cancelled [cancelled on failure]" } ] }, "status" : 500 } + curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx04 | eval x = textfield | where match(x, \"live\")" } ' textfield | textfield.keyword | x -------------------------------------+-------------------------------------+------------------------------------- live long and prosper 04 text keyword|live long and prosper 04 text keyword|live long and prosper 04 text keyword 
)
);
} else {
failures.add(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about it more, I wonder if we should allow multiple inference ids on the same field, if the inference ids come from different indices, and each index has only one inference id on this field. This is more like a usability issue.

If I have multiple indices that have the same semantic text field name, I couldn't find a way to query them in at once. Does it look like a valid use case? Schema is here.

Thanks for fixing this! + curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx00, testidx01 | where match(textfield, \"live\")" } ' { "error" : { "root_cause" : [ { "type" : "verification_exception", "reason" : "Found 1 problem\nline 1:41: [MATCH] function cannot operate on [textfield] because it is configured with multiple inference IDs." } ], "type" : "verification_exception", "reason" : "Found 1 problem\nline 1:41: [MATCH] function cannot operate on [textfield] because it is configured with multiple inference IDs." }, "status" : 400 } + curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx00, testidx01 | where match(textfield::keyword, \"live\") | eval k = textfield::keyword | keep k" } ' { "error" : { "root_cause" : [ { "type" : "verification_exception", "reason" : "Found 1 problem\nline 1:41: [MATCH] function cannot operate on [textfield::keyword], which is not a field from an index mapping" } ], "type" : "verification_exception", "reason" : "Found 1 problem\nline 1:41: [MATCH] function cannot operate on [textfield::keyword], which is not a field from an index mapping" }, "status" : 400 } 
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fang-xing-esql , the problem with multiple inference IDs is relevance. It does not make sense to mix multiple inference IDs from the scoring point of view, as different models will get different scores that can't directly be combined. That's the main reason it is not allowed on the _search endpoint.

In _search, users can combine different inference ids using different query clauses in _search, that can the be further adjusted via boosting.

Even if it's technically possible to do this from a filtering PoV, it won't make sense to do it once we combine with score.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we take this as a follow up item?
Since this restriction is also present in _search, for all the reasons @carlosdelest mentioned, I thought having it in ES|QL is reasonable for now.
The time where it becomes relevant to lift this restriction is when we introduce RRF, in that case we might want to allow RRF between two subqueries that query the same field on different indices: WHERE where match(textfield, "live") AND _index == "testidx00" and WHERE where match(textfield, "live") AND _index == "testidx01".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this can be addressed as a follow up.

match.inferenceResults()
);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is another scenario that I can think of - there are two indices with the same field name, one is semantic text, the other is a text field for example. If I query each of them individually, I got expected results, however if I query both of them together, I got empty results. It is similar to an issue that I brought up to Carlos on his PR, however, semantic text has a different type of query for now, perhaps it has to be taken care of in a different way.

+ curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx00 | where match(textfield, \"live\")" } ' textfield ------------------------------ live long and prosper 00 elser + curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx04 | where match(textfield, \"live\")" } ' textfield | textfield.keyword -------------------------------------+------------------------------------- live long and prosper 04 text keyword|live long and prosper 04 text keyword + curl -u elastic:password -v -X POST 'localhost:9200/_query?format=txt&pretty' -H 'Content-Type: application/json' '-d { "query": "from testidx00, testidx04 | where match(textfield::keyword, \"live\") | eval k = textfield::keyword | keep k" } ' k --------------- 
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find! I added this as a follow up of things to do before we take this out of snapshot in #115103.

@ioanatia
Copy link
Contributor Author

closed in favour of #118676

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged >non-issue Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) test-release Trigger CI checks against release build v8.18.0 v9.0.0

4 participants