diff --git a/docs/reference/query-languages/esql/images/functions/knn.svg b/docs/reference/query-languages/esql/images/functions/knn.svg
new file mode 100644
index 000000000000..75a104a7cdcf
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/knn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/knn.json b/docs/reference/query-languages/esql/kibana/definition/functions/knn.json
new file mode 100644
index 000000000000..48d3e582eec5
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/knn.json
@@ -0,0 +1,13 @@
+{
+ "comment" : "This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+ "type" : "scalar",
+ "name" : "knn",
+ "description" : "Finds the k nearest vectors to a query vector, as measured by a similarity metric. knn function finds nearest vectors through approximate search on indexed dense_vectors.",
+ "signatures" : [ ],
+ "examples" : [
+ "from colors metadata _score\n| where knn(rgb_vector, [0, 120, 0])\n| sort _score desc",
+ "from colors metadata _score\n| where knn(rgb_vector, [0,255,255], {\"k\": 4})\n| sort _score desc"
+ ],
+ "preview" : true,
+ "snapshot_only" : true
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/knn.md b/docs/reference/query-languages/esql/kibana/docs/functions/knn.md
new file mode 100644
index 000000000000..45d1f294ea0a
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/knn.md
@@ -0,0 +1,10 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+### KNN
+Finds the k nearest vectors to a query vector, as measured by a similarity metric. knn function finds nearest vectors through approximate search on indexed dense_vectors.
+
+```esql
+from colors metadata _score
+| where knn(rgb_vector, [0, 120, 0])
+| sort _score desc
+```
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java
index 40c69ec6e7fd..9360d13c7833 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java
@@ -2589,6 +2589,11 @@ public class DenseVectorFieldMapper extends FieldMapper {
return null;
}
+ if (dims == null) {
+ // No data has been indexed yet
+ return BlockLoader.CONSTANT_NULLS;
+ }
+
if (indexed) {
return new BlockDocValuesReader.DenseVectorBlockLoader(name(), dims);
}
diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluator.java
index 4644dd31f204..d91df60621fc 100644
--- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluator.java
+++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneQueryEvaluator.java
@@ -112,7 +112,7 @@ public abstract class LuceneQueryEvaluator implements
int min = docs.docs().getInt(0);
int max = docs.docs().getInt(docs.getPositionCount() - 1);
int length = max - min + 1;
- try (T scoreBuilder = createVectorBuilder(blockFactory, length)) {
+ try (T scoreBuilder = createVectorBuilder(blockFactory, docs.getPositionCount())) {
if (length == docs.getPositionCount() && length > 1) {
return segmentState.scoreDense(scoreBuilder, min, max);
}
diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java
index ce13a655966c..293200b22791 100644
--- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java
+++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java
@@ -1022,7 +1022,7 @@ public abstract class RestEsqlTestCase extends ESRestTestCase {
var query = requestObjectBuilder().query(format(null, "from * | lookup join {} on integer {}", testIndexName(), sort));
Map result = runEsql(query);
var columns = as(result.get("columns"), List.class);
- assertEquals(21, columns.size());
+ assertEquals(22, columns.size());
var values = as(result.get("values"), List.class);
assertEquals(10, values.size());
}
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java
index 2d06431806ab..f1eb32afbdfe 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java
@@ -148,6 +148,7 @@ public class CsvTestsDataLoader {
private static final TestDataset LOGS = new TestDataset("logs");
private static final TestDataset MV_TEXT = new TestDataset("mv_text");
private static final TestDataset DENSE_VECTOR = new TestDataset("dense_vector");
+ private static final TestDataset COLORS = new TestDataset("colors");
public static final Map CSV_DATASET_MAP = Map.ofEntries(
Map.entry(EMPLOYEES.indexName, EMPLOYEES),
@@ -210,7 +211,8 @@ public class CsvTestsDataLoader {
Map.entry(SEMANTIC_TEXT.indexName, SEMANTIC_TEXT),
Map.entry(LOGS.indexName, LOGS),
Map.entry(MV_TEXT.indexName, MV_TEXT),
- Map.entry(DENSE_VECTOR.indexName, DENSE_VECTOR)
+ Map.entry(DENSE_VECTOR.indexName, DENSE_VECTOR),
+ Map.entry(COLORS.indexName, COLORS)
);
private static final EnrichConfig LANGUAGES_ENRICH = new EnrichConfig("languages_policy", "enrich-policy-languages.json");
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/colors.csv b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/colors.csv
new file mode 100644
index 000000000000..b82ef7087a54
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/data/colors.csv
@@ -0,0 +1,60 @@
+color:text,hex_code:keyword,rgb_vector:dense_vector,primary:boolean
+maroon, #800000, [128,0,0], false
+brown, #A52A2A, [165,42,42], false
+firebrick, #B22222, [178,34,34], false
+crimson, #DC143C, [220,20,60], false
+red, #FF0000, [255,0,0], true
+tomato, #FF6347, [255,99,71], false
+coral, #FF7F50, [255,127,80], false
+salmon, #FA8072, [250,128,114], false
+orange, #FFA500, [255,165,0], false
+gold, #FFD700, [255,215,0], false
+golden rod, #DAA520, [218,165,32], false
+khaki, #F0E68C, [240,230,140], false
+olive, #808000, [128,128,0], false
+yellow, #FFFF00, [255,255,0], true
+chartreuse, #7FFF00, [127,255,0], false
+green, #008000, [0,128,0], true
+lime, #00FF00, [0,255,0], false
+teal, #008080, [0,128,128], false
+cyan, #00FFFF, [0,255,255], true
+turquoise, #40E0D0, [64,224,208], false
+aqua marine, #7FFFD4, [127,255,212], false
+navy, #000080, [0,0,128], false
+blue, #0000FF, [0,0,255], true
+indigo, #4B0082, [75,0,130], false
+purple, #800080, [128,0,128], false
+thistle, #D8BFD8, [216,191,216], false
+plum, #DDA0DD, [221,160,221], false
+violet, #EE82EE, [238,130,238], false
+magenta, #FF00FF, [255,0,255], true
+orchid, #DA70D6, [218,112,214], false
+pink, #FFC0CB, [255,192,203], false
+beige, #F5F5DC, [245,245,220], false
+bisque, #FFE4C4, [255,228,196], false
+wheat, #F5DEB3, [245,222,179], false
+corn silk, #FFF8DC, [255,248,220], false
+lemon chiffon, #FFFACD, [255,250,205], false
+sienna, #A0522D, [160,82,45], false
+chocolate, #D2691E, [210,105,30], false
+peru, #CD853F, [205,133,63], false
+burly wood, #DEB887, [222,184,135], false
+tan, #D2B48C, [210,180,140], false
+moccasin, #FFE4B5, [255,228,181], false
+peach puff, #FFDAB9, [255,218,185], false
+misty rose, #FFE4E1, [255,228,225], false
+linen, #FAF0E6, [250,240,230], false
+old lace, #FDF5E6, [253,245,230], false
+papaya whip, #FFEFD5, [255,239,213], false
+sea shell, #FFF5EE, [255,245,238], false
+mint cream, #F5FFFA, [245,255,250], false
+lavender, #E6E6FA, [230,230,250], false
+honeydew, #F0FFF0, [240,255,240], false
+ivory, #FFFFF0, [255,255,240], false
+azure, #F0FFFF, [240,255,255], false
+snow, #FFFAFA, [255,250,250], false
+black, #000000, [0,0,0], true
+gray, #808080, [128,128,128], true
+silver, #C0C0C0, [192,192,192], false
+gainsboro, #DCDCDC, [220,220,220], false
+white, #FFFFFF, [255,255,255], true
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/knn-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/knn-function.csv-spec
new file mode 100644
index 000000000000..5e65e6269e65
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/knn-function.csv-spec
@@ -0,0 +1,285 @@
+# TODO Most tests explicitly set k. Until knn function uses LIMIT as k, we need to explicitly set it to all values
+# in the dataset to avoid test failures due to docs allocation in different shards, which can impact results for a
+# top-n query at the shard level
+
+knnSearch
+required_capability: knn_function
+
+// tag::knn-function[]
+from colors metadata _score
+| where knn(rgb_vector, [0, 120, 0])
+| sort _score desc, color asc
+// end::knn-function[]
+| keep color, rgb_vector
+| limit 10
+;
+
+// tag::knn-function-result[]
+color:text | rgb_vector:dense_vector
+green | [0.0, 128.0, 0.0]
+black | [0.0, 0.0, 0.0]
+olive | [128.0, 128.0, 0.0]
+teal | [0.0, 128.0, 128.0]
+lime | [0.0, 255.0, 0.0]
+sienna | [160.0, 82.0, 45.0]
+maroon | [128.0, 0.0, 0.0]
+navy | [0.0, 0.0, 128.0]
+gray | [128.0, 128.0, 128.0]
+chartreuse | [127.0, 255.0, 0.0]
+// end::knn-function-result[]
+;
+
+knnSearchWithKOption
+required_capability: knn_function
+
+// tag::knn-function-options[]
+from colors metadata _score
+| where knn(rgb_vector, [0,255,255], {"k": 4})
+| sort _score desc, color asc
+// end::knn-function-options[]
+| keep color, rgb_vector
+| limit 4
+;
+
+color:text | rgb_vector:dense_vector
+cyan | [0.0, 255.0, 255.0]
+turquoise | [64.0, 224.0, 208.0]
+aqua marine | [127.0, 255.0, 212.0]
+teal | [0.0, 128.0, 128.0]
+;
+
+knnSearchWithSimilarityOption
+required_capability: knn_function
+
+from colors metadata _score
+| where knn(rgb_vector, [255,192,203], {"k": 140, "similarity": 40})
+| sort _score desc, color asc
+| keep color, rgb_vector
+;
+
+color:text | rgb_vector:dense_vector
+pink | [255.0, 192.0, 203.0]
+peach puff | [255.0, 218.0, 185.0]
+bisque | [255.0, 228.0, 196.0]
+wheat | [245.0, 222.0, 179.0]
+
+;
+
+knnHybridSearch
+required_capability: knn_function
+
+from colors metadata _score
+| where match(color, "blue") or knn(rgb_vector, [65,105,225], {"k": 140})
+| where primary == true
+| sort _score desc, color asc
+| keep color, rgb_vector
+| limit 10
+;
+
+color:text | rgb_vector:dense_vector
+blue | [0.0, 0.0, 255.0]
+gray | [128.0, 128.0, 128.0]
+cyan | [0.0, 255.0, 255.0]
+magenta | [255.0, 0.0, 255.0]
+green | [0.0, 128.0, 0.0]
+white | [255.0, 255.0, 255.0]
+black | [0.0, 0.0, 0.0]
+red | [255.0, 0.0, 0.0]
+yellow | [255.0, 255.0, 0.0]
+;
+
+knnWithMultipleFunctions
+required_capability: knn_function
+
+from colors metadata _score
+| where knn(rgb_vector, [128,128,0], {"k": 140}) and match(color, "olive")
+| sort _score desc, color asc
+| keep color, rgb_vector
+;
+
+color:text | rgb_vector:dense_vector
+olive | [128.0, 128.0, 0.0]
+;
+
+knnAfterKeep
+required_capability: knn_function
+
+from colors metadata _score
+| keep rgb_vector, color, _score
+| where knn(rgb_vector, [128,255,0], {"k": 140})
+| sort _score desc, color asc
+| keep rgb_vector
+| limit 5
+;
+
+rgb_vector:dense_vector
+[127.0, 255.0, 0.0]
+[128.0, 128.0, 0.0]
+[255.0, 255.0, 0.0]
+[0.0, 255.0, 0.0]
+[218.0, 165.0, 32.0]
+;
+
+knnAfterDrop
+required_capability: knn_function
+
+from colors metadata _score
+| drop primary
+| where knn(rgb_vector, [128,250,0], {"k": 140})
+| sort _score desc, color asc
+| keep color, rgb_vector
+| limit 5
+;
+
+color:text | rgb_vector: dense_vector
+chartreuse | [127.0, 255.0, 0.0]
+olive | [128.0, 128.0, 0.0]
+yellow | [255.0, 255.0, 0.0]
+golden rod | [218.0, 165.0, 32.0]
+lime | [0.0, 255.0, 0.0]
+;
+
+knnAfterEval
+required_capability: knn_function
+
+from colors metadata _score
+| eval composed_name = locate(color, " ") > 0
+| where knn(rgb_vector, [128,128,0], {"k": 140})
+| sort _score desc, color asc
+| keep color, composed_name
+| limit 5
+;
+
+color:text | composed_name:boolean
+olive | false
+sienna | false
+chocolate | false
+peru | false
+golden rod | true
+;
+
+knnWithConjunction
+required_capability: knn_function
+
+# TODO We need kNN prefiltering here so we get more candidates that pass the filter
+from colors metadata _score
+| where knn(rgb_vector, [255,255,238], {"k": 140}) and hex_code like "#FFF*"
+| sort _score desc, color asc
+| keep color, hex_code, rgb_vector
+| limit 10
+;
+
+color:text | hex_code:keyword | rgb_vector:dense_vector
+ivory | #FFFFF0 | [255.0, 255.0, 240.0]
+sea shell | #FFF5EE | [255.0, 245.0, 238.0]
+snow | #FFFAFA | [255.0, 250.0, 250.0]
+white | #FFFFFF | [255.0, 255.0, 255.0]
+corn silk | #FFF8DC | [255.0, 248.0, 220.0]
+lemon chiffon | #FFFACD | [255.0, 250.0, 205.0]
+yellow | #FFFF00 | [255.0, 255.0, 0.0]
+;
+
+knnWithDisjunctionAndFiltersConjunction
+required_capability: knn_function
+
+# TODO We need kNN prefiltering here so we get more candidates that pass the filter
+from colors metadata _score
+| where (knn(rgb_vector, [0,255,255], {"k": 140}) or knn(rgb_vector, [128, 0, 255], {"k": 140})) and primary == true
+| keep color, rgb_vector, _score
+| sort _score desc, color asc
+| drop _score
+| limit 10
+;
+
+color:text | rgb_vector:dense_vector
+cyan | [0.0, 255.0, 255.0]
+blue | [0.0, 0.0, 255.0]
+magenta | [255.0, 0.0, 255.0]
+gray | [128.0, 128.0, 128.0]
+white | [255.0, 255.0, 255.0]
+green | [0.0, 128.0, 0.0]
+black | [0.0, 0.0, 0.0]
+red | [255.0, 0.0, 0.0]
+yellow | [255.0, 255.0, 0.0]
+;
+
+knnWithNonPushableConjunction
+required_capability: knn_function
+
+from colors metadata _score
+| eval composed_name = locate(color, " ") > 0
+| where knn(rgb_vector, [128,128,0], {"k": 140}) and composed_name == false
+| sort _score desc, color asc
+| keep color, composed_name
+| limit 10
+;
+
+color:text | composed_name:boolean
+olive | false
+sienna | false
+chocolate | false
+peru | false
+brown | false
+firebrick | false
+chartreuse | false
+gray | false
+green | false
+maroon | false
+;
+
+testKnnWithNonPushableDisjunctions
+required_capability: knn_function
+
+from colors metadata _score
+| where knn(rgb_vector, [128,128,0], {"k": 140, "similarity": 30}) or length(color) > 10
+| sort _score desc, color asc
+| keep color
+;
+
+color:text
+olive
+aqua marine
+lemon chiffon
+papaya whip
+;
+
+testKnnWithNonPushableDisjunctionsOnComplexExpressions
+required_capability: knn_function
+
+from colors metadata _score
+| where (knn(rgb_vector, [128,128,0], {"k": 140, "similarity": 70}) and length(color) < 10) or (knn(rgb_vector, [128,0,128], {"k": 140, "similarity": 60}) and primary == false)
+| sort _score desc, color asc
+| keep color, primary
+;
+
+color:text | primary:boolean
+olive | false
+purple | false
+indigo | false
+;
+
+testKnnInStatsNonPushable
+required_capability: knn_function
+
+from colors
+| where length(color) < 10
+| stats c = count(*) where knn(rgb_vector, [128,128,255], {"k": 140})
+;
+
+c: long
+50
+;
+
+testKnnInStatsWithGrouping
+required_capability: knn_function
+required_capability: full_text_functions_in_stats_where
+
+from colors
+| where length(color) < 10
+| stats c = count(*) where knn(rgb_vector, [128,128,255], {"k": 140}) by primary
+;
+
+c: long | primary: boolean
+41 | false
+9 | true
+;
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json
index 17348adb6af4..a7ef2f484070 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json
@@ -63,6 +63,9 @@
"semantic_text": {
"type": "semantic_text",
"inference_id": "foo_inference_id"
+ },
+ "dense_vector": {
+ "type": "dense_vector"
}
}
}
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json
new file mode 100644
index 000000000000..24c4102e428f
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-colors.json
@@ -0,0 +1,20 @@
+{
+ "properties": {
+ "color": {
+ "type": "text"
+ },
+ "hex_code": {
+ "type": "keyword"
+ },
+ "primary": {
+ "type": "boolean"
+ },
+ "rgb_vector": {
+ "type": "dense_vector",
+ "similarity": "l2_norm",
+ "index_options": {
+ "type": "hnsw"
+ }
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/DenseVectorFieldTypeIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/DenseVectorFieldTypeIT.java
index 905cf413fb48..a130b026cd88 100644
--- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/DenseVectorFieldTypeIT.java
+++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/DenseVectorFieldTypeIT.java
@@ -128,10 +128,57 @@ public class DenseVectorFieldTypeIT extends AbstractEsqlIntegTestCase {
}
}
+ public void testNonIndexedDenseVectorField() throws IOException {
+ createIndexWithDenseVector("no_dense_vectors");
+
+ int numDocs = randomIntBetween(10, 100);
+ IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs];
+ for (int i = 0; i < numDocs; i++) {
+ docs[i] = prepareIndex("no_dense_vectors").setId("" + i).setSource("id", String.valueOf(i));
+ }
+
+ indexRandom(true, docs);
+
+ var query = """
+ FROM no_dense_vectors
+ | KEEP id, vector
+ """;
+
+ try (var resp = run(query)) {
+ List> valuesList = EsqlTestUtils.getValuesList(resp);
+ assertEquals(numDocs, valuesList.size());
+ valuesList.forEach(value -> {
+ assertEquals(2, value.size());
+ Integer id = (Integer) value.get(0);
+ assertNotNull(id);
+ Object vector = value.get(1);
+ assertNull(vector);
+ });
+ }
+ }
+
@Before
public void setup() throws IOException {
assumeTrue("Dense vector type is disabled", EsqlCapabilities.Cap.DENSE_VECTOR_FIELD_TYPE.isEnabled());
- var indexName = "test";
+
+ createIndexWithDenseVector("test");
+
+ int numDims = randomIntBetween(32, 64) * 2; // min 64, even number
+ int numDocs = randomIntBetween(10, 100);
+ IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs];
+ for (int i = 0; i < numDocs; i++) {
+ List vector = new ArrayList<>(numDims);
+ for (int j = 0; j < numDims; j++) {
+ vector.add(randomFloat());
+ }
+ docs[i] = prepareIndex("test").setId("" + i).setSource("id", String.valueOf(i), "vector", vector);
+ indexedVectors.put(i, vector);
+ }
+
+ indexRandom(true, docs);
+ }
+
+ private void createIndexWithDenseVector(String indexName) throws IOException {
var client = client().admin().indices();
XContentBuilder mapping = XContentFactory.jsonBuilder()
.startObject()
@@ -161,19 +208,5 @@ public class DenseVectorFieldTypeIT extends AbstractEsqlIntegTestCase {
.setMapping(mapping)
.setSettings(settingsBuilder.build());
assertAcked(CreateRequest);
-
- int numDims = randomIntBetween(32, 64) * 2; // min 64, even number
- int numDocs = randomIntBetween(10, 100);
- IndexRequestBuilder[] docs = new IndexRequestBuilder[numDocs];
- for (int i = 0; i < numDocs; i++) {
- List vector = new ArrayList<>(numDims);
- for (int j = 0; j < numDims; j++) {
- vector.add(randomFloat());
- }
- docs[i] = prepareIndex("test").setId("" + i).setSource("id", String.valueOf(i), "vector", vector);
- indexedVectors.put(i, vector);
- }
-
- indexRandom(true, docs);
}
}
diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KnnFunctionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KnnFunctionIT.java
new file mode 100644
index 000000000000..a26294390993
--- /dev/null
+++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/KnnFunctionIT.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.plugin;
+
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentFactory;
+import org.elasticsearch.xpack.esql.EsqlTestUtils;
+import org.elasticsearch.xpack.esql.action.AbstractEsqlIntegTestCase;
+import org.elasticsearch.xpack.esql.action.EsqlCapabilities;
+import org.junit.Before;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+
+public class KnnFunctionIT extends AbstractEsqlIntegTestCase {
+
+ private final Map> indexedVectors = new HashMap<>();
+ private int numDocs;
+ private int numDims;
+
+ public void testKnnDefaults() {
+ float[] queryVector = new float[numDims];
+ Arrays.fill(queryVector, 1.0f);
+
+ var query = String.format(Locale.ROOT, """
+ FROM test METADATA _score
+ | WHERE knn(vector, %s)
+ | KEEP id, floats, _score, vector
+ | SORT _score DESC
+ """, Arrays.toString(queryVector));
+
+ try (var resp = run(query)) {
+ assertColumnNames(resp.columns(), List.of("id", "floats", "_score", "vector"));
+ assertColumnTypes(resp.columns(), List.of("integer", "double", "double", "dense_vector"));
+
+ List> valuesList = EsqlTestUtils.getValuesList(resp);
+ assertEquals(Math.min(indexedVectors.size(), 10), valuesList.size());
+ for (int i = 0; i < valuesList.size(); i++) {
+ List