k-NN Warm-Up: From User to Contributor
"Space," said the k-NN plugin, "is big. Really big. You just won't believe how vastly, hugely, mind-bogglingly big it is. I mean, you may think it's a long way down the road to the chemist's, but that's just peanuts to a 1,024-dimensional embedding space."
Don't panic. You are about to do something that sounds impossible — search a space with
a thousand axes, find the handful of points nearest to where you're standing, and do
it in a few milliseconds over a hundred million neighbors — and by the end of this
chapter you'll have done it from a terminal, watched the off-heap memory light up, and
traced every observable behavior back to the exact org.opensearch.knn.* class that
owns it. This is the missing first mile for the k-NN plugin, the way
the OpenSearch warm-up was the missing first mile for the core
engine. Same idea: sit in the user's seat first, then open the source, so the class
names land on something real instead of floating free.
If you haven't read the section overview yet, skim it — it tells you what
vector search is for and how k-NN bolts onto core OpenSearch. This chapter assumes a
local cluster with the k-NN plugin installed (the build-from-source lab
walks you through it; for now, ./gradlew run in a k-NN checkout gives you a node with
the plugin already on it, REST on localhost:9200).
The Guide's one piece of advice for the journey: vector search is just normal
OpenSearch with a strange field type and a strange query. Everything you already know
— shards, segments, the search fan-out, the coordinating-node reduce — still applies. A
knn_vector is a field. A knn query is a QueryBuilder. The exotic part is what
happens inside one shard, in C++, off the JVM heap. Hold that, and the whole thing
stops being intimidating.
The Babel fish: embeddings turn meaning into geometry
Before any curl, the one concept that makes the rest make sense. An embedding model
(BERT, a sentence-transformer, OpenAI's text-embedding-3, CLIP for images — pick your
poison) is a function that eats text or an image and emits a fixed-length float[] — a
vector. The magic property: things that mean similar things land near each other.
"a small domestic feline" and "kitten" share zero tokens, so BM25 thinks they're
strangers; their embeddings sit a whisker apart. The embedding is a Babel fish for
meaning: it translates "what this is about" into "where this is in space," and once
meaning is geometry, "find similar" becomes "find nearest."
"a small domestic feline" --[embed]--> [0.021, -0.88, 0.13, ... ] (768 floats)
"kitten" --[embed]--> [0.019, -0.85, 0.15, ... ] <- nearly the same point
"quarterly tax filing" --[embed]--> [0.77, 0.04, -0.6, ... ] <- far away
You produce these vectors outside the query (an external model, or inside OpenSearch
via the ml-commons plugin and a neural-search ingest pipeline). k-NN's job starts the
moment you have a float[]: store it, index it for fast nearest-neighbor lookup, and at
query time find the k stored vectors closest to a query vector by some distance metric.
That's the whole contract. Everything else — engines, graphs, quantization, off-heap
memory — is machinery to make "find the nearest k" fast and affordable at scale.
Note: "closest" needs a definition. The
space_typemapping parameter picks the distance metric:l2(Euclidean),cosinesimil(cosine),innerproduct,l1,linf, andhamming(for binary vectors). Pick the one your embedding model was trained for — usingl2on vectors meant for cosine quietly wrecks recall, and it is a top-five user mistake.
Scenario 0: prove the plugin is alive
Two sentences of due diligence before we index anything. Confirm the plugin loaded and the stats endpoint answers — if these fail, nothing below will work and you should go back to the build lab.
# Is the k-NN plugin actually installed on this node?
curl -s 'localhost:9200/_cat/plugins?v' | grep -i knn
# The k-NN stats endpoint — the dashboard for everything off-heap.
curl -s 'localhost:9200/_plugins/_knn/stats?pretty' | head -40
The _cat/plugins line names opensearch-knn; _plugins/_knn/stats returns a JSON
blob of counters — graph memory used, cache hits/misses, circuit-breaker state, query
counts. Most of it reads 0 right now. By the end of this chapter every interesting
counter will have moved, and you'll know which line of which class moved it.
Scenario 1: basic approximate k-NN (the "hello, vectors" path)
What the user does — create a knn-enabled index with a small knn_vector field,
bulk-index a handful of vectors, run a knn query for the nearest k.
# 1. Create the index. THREE things make it a vector index:
# index.knn:true, type knn_vector, and a method (hnsw on faiss = the default engine).
curl -s -XPUT 'localhost:9200/cats' -H 'Content-Type: application/json' -d '
{
"settings": { "index.knn": true },
"mappings": {
"properties": {
"embedding": {
"type": "knn_vector",
"dimension": 4,
"space_type": "l2",
"method": {
"name": "hnsw",
"engine": "faiss",
"parameters": { "m": 16, "ef_construction": 128 }
}
},
"label": { "type": "keyword" }
}
}
}'
# 2. Bulk-index five 4-dimensional vectors. (Real ones are 384/768/1024-dim; 4-dim
# fits on a page and the geometry is identical.)
curl -s -H 'Content-Type: application/x-ndjson' \
-XPOST 'localhost:9200/cats/_bulk?refresh=true' --data-binary $'
{"index":{"_id":"1"}}
{"label":"tabby", "embedding":[0.10, 0.20, 0.30, 0.40]}
{"index":{"_id":"2"}}
{"label":"siamese", "embedding":[0.11, 0.19, 0.31, 0.39]}
{"index":{"_id":"3"}}
{"label":"lion", "embedding":[0.90, 0.80, 0.10, 0.05]}
{"index":{"_id":"4"}}
{"label":"tiger", "embedding":[0.88, 0.82, 0.12, 0.04]}
{"index":{"_id":"5"}}
{"label":"sphynx", "embedding":[0.12, 0.21, 0.29, 0.41]}
'
# 3. Find the 3 nearest neighbors to a query vector near the "house cat" cluster.
curl -s 'localhost:9200/cats/_search?pretty' -H 'Content-Type: application/json' -d '
{
"size": 3,
"query": {
"knn": {
"embedding": {
"vector": [0.10, 0.20, 0.30, 0.40],
"k": 3
}
}
}
}'
You get back docs 1, 5, 2 (the house-cat cluster), each with a _score. The lion
and tiger are nowhere — they live in a different neighborhood. Note the _score is a
similarity (higher = nearer), not a raw distance; k-NN converts the engine's distance
into a monotonically-increasing score so it composes with normal OpenSearch scoring.
What k-NN does under the hood:
- At mapping time,
KNNVectorFieldMapper.Builderparsesdimension,space_type, and themethodobject, validates them against the chosenKNNEngine(faiss), and produces aKNNVectorFieldType. This is aMapperPlugincontribution — the same extension point any custom field type uses (see plugin architecture). - On index, each
float[]is stored, and at flush/merge a custom Lucene codec (KNN1030Codecand itsNativeEngines990KnnVectorsFormat, in the current source — grep to confirm the version) calls into faiss over JNI to build an HNSW graph and write it as segment files alongside Lucene's normal files. - The
knnquery parses into aKNNQueryBuilder, which builds aKNNQuery. Per shard,KNNWeight/KNNScorerask the loaded faiss graph for the nearestkdoc IDs and their distances, convert distance → score, and feed them into the normal Lucene collector. The coordinating node merges the per-shard top-kexactly like any search.
Bridge to source:
cd ~/src/oss-repos/k-NN # your k-NN checkout
# The field mapper and field type (MapperPlugin contribution)
find src/main/java -name "KNNVectorFieldMapper.java" -o -name "KNNVectorFieldType.java"
# The query path
find src/main/java -name "KNNQueryBuilder.java" -o -name "KNNQuery.java" -o -name "KNNWeight.java"
# The custom codec that writes the graph as segment files
find src/main/java -path "*codec*" -name "KNN*Codec.java" | grep -v backward
find src/main/java -name "NativeEngines990KnnVectorsFormat.java"
Scenario 2: filtered k-NN (vectors that also obey a term)
What the user does — "nearest neighbors, but only among indoor cats." Pure ANN ignores metadata; real applications almost always need to combine semantic similarity with a structured filter.
# Re-create the index with a boolean we can filter on.
curl -s -XDELETE 'localhost:9200/cats' >/dev/null
curl -s -XPUT 'localhost:9200/cats' -H 'Content-Type: application/json' -d '
{
"settings": { "index.knn": true },
"mappings": {
"properties": {
"embedding": { "type": "knn_vector", "dimension": 4, "space_type": "l2",
"method": { "name": "hnsw", "engine": "faiss" } },
"indoor": { "type": "boolean" },
"label": { "type": "keyword" }
}
}
}'
curl -s -H 'Content-Type: application/x-ndjson' \
-XPOST 'localhost:9200/cats/_bulk?refresh=true' --data-binary $'
{"index":{}}
{"label":"tabby","indoor":true, "embedding":[0.10,0.20,0.30,0.40]}
{"index":{}}
{"label":"siamese","indoor":true,"embedding":[0.11,0.19,0.31,0.39]}
{"index":{}}
{"label":"feral","indoor":false,"embedding":[0.10,0.20,0.30,0.41]}
'
# k-NN with a filter: nearest indoor cats only.
curl -s 'localhost:9200/cats/_search?pretty' -H 'Content-Type: application/json' -d '
{
"size": 3,
"query": {
"knn": {
"embedding": {
"vector": [0.10, 0.20, 0.30, 0.40],
"k": 3,
"filter": { "term": { "indoor": true } }
}
}
}
}'
The nearest vector belongs to feral, but it's indoor:false, so it's excluded — you
get the indoor cats. Crucially, the filter is applied during the graph traversal, not
naively after: faiss and lucene support efficient (pre-)filtering so the k you ask
for is k matching results, not k candidates that mostly get thrown away.
Why this is subtle: naive post-filtering breaks ANN. If you fetch the top-k then
filter, and your filter is selective, you can get zero results even though matching
neighbors exist further down the graph. So the engine pushes the filter into the
traversal. This is exactly why filtering support differs by engine —
ENGINES_SUPPORTING_FILTERS in KNNEngine is {LUCENE, FAISS}, and nmslib is not in
it. The engines chapter covers why.
Bridge to source:
# Filter handling in the query path — how the term filter becomes a BitSet the
# engine traverses against.
grep -rn "filter\|FilteredIdsKNNIterator\|BitSet\|Weight" \
src/main/java/org/opensearch/knn/index/query/KNNWeight.java | head
# The engine capability set that decides whether filtering is even allowed.
grep -n "ENGINES_SUPPORTING_FILTERS" src/main/java/org/opensearch/knn/index/engine/KNNEngine.java
Scenario 3: byte vectors (the memory diet)
What the user does — index byte vectors instead of float. Each dimension is a
single signed byte (-128..127) instead of a 4-byte float — a 4× memory reduction
before any fancier quantization. Supported on faiss and lucene since 2.17.
curl -s -XPUT 'localhost:9200/cats-byte' -H 'Content-Type: application/json' -d '
{
"settings": { "index.knn": true },
"mappings": {
"properties": {
"embedding": {
"type": "knn_vector",
"dimension": 4,
"data_type": "byte",
"space_type": "l2",
"method": { "name": "hnsw", "engine": "lucene" }
}
}
}
}'
curl -s -H 'Content-Type: application/x-ndjson' \
-XPOST 'localhost:9200/cats-byte/_bulk?refresh=true' --data-binary $'
{"index":{}}
{"embedding":[10, 20, 30, 40]}
{"index":{}}
{"embedding":[12, 19, 31, 39]}
'
curl -s 'localhost:9200/cats-byte/_search?pretty' -H 'Content-Type: application/json' -d '
{ "size": 2, "query": { "knn": { "embedding": { "vector": [10,20,30,40], "k": 2 } } } }'
The values are now integers in [-128, 127]. You traded precision for memory; recall
drops a little, RAM drops a lot. This is the gentlest rung on a whole quantization
ladder — FP16 scalar quantization, Product Quantization (PQ), Binary Quantization (BQ),
and full disk-based search — covered in
quantization and disk-ANN. The point of trying it now is
to see data_type change the field's behavior.
Bridge to source:
# The data-type enum: BINARY, BYTE, FLOAT.
grep -nE "BINARY|BYTE|FLOAT|DEFAULT" src/main/java/org/opensearch/knn/index/VectorDataType.java | head
# Where the mapper branches on data_type to pick the storage/encoding.
grep -rn "VectorDataType\|data_type" src/main/java/org/opensearch/knn/index/mapper/ | head
Scenario 4: radial search (a threshold, not a count)
What the user does — instead of "give me the k nearest," ask "give me everyone
within this radius" — every vector closer than a distance threshold (or above a score
threshold). This is the right shape for deduplication ("anything within 0.05 is a
near-duplicate") and anomaly detection ("nothing within 0.5 → outlier").
# max_distance: return all neighbors within this L2 distance of the query.
curl -s 'localhost:9200/cats/_search?pretty' -H 'Content-Type: application/json' -d '
{
"size": 10,
"query": {
"knn": {
"embedding": {
"vector": [0.10, 0.20, 0.30, 0.40],
"max_distance": 0.05
}
}
}
}'
# Equivalently, a score floor (min_score) instead of a distance ceiling.
curl -s 'localhost:9200/cats/_search?pretty' -H 'Content-Type: application/json' -d '
{
"size": 10,
"query": {
"knn": { "embedding": { "vector": [0.10,0.20,0.30,0.40], "min_score": 0.95 } }
}
}'
Note there is no k here — the result set size is whatever falls inside the
radius, so a tight radius can return nothing and a loose one can return everything.
Radial search is supported on lucene and faiss (ENGINES_SUPPORTING_RADIAL_SEARCH
in KNNEngine), not nmslib.
Bridge to source:
# min_score / max_distance parsing and the radial query branch.
grep -rn "max_distance\|min_score\|RadialSearch\|radial\|MAX_DISTANCE\|MIN_SCORE" \
src/main/java/org/opensearch/knn/index/query/KNNQueryBuilder.java | head
grep -n "ENGINES_SUPPORTING_RADIAL_SEARCH" src/main/java/org/opensearch/knn/index/engine/KNNEngine.java
Scenario 5: exact vs approximate (the script-score escape hatch)
What the user does — sometimes you want the exact nearest neighbors (100% recall,
no graph), usually as a rescoring pass over a small candidate set, or on a field that
isn't graph-indexed at all. k-NN exposes this as a knn script score — brute-force
distance over every matching doc, scored by a knn_score script.
# Exact k-NN: score EVERY doc by L2 distance to the query (a full scan).
# No graph, no approximation, 100% recall — and O(N), so only sane after a filter.
curl -s 'localhost:9200/cats/_search?pretty' -H 'Content-Type: application/json' -d '
{
"size": 3,
"query": {
"script_score": {
"query": { "match_all": {} },
"script": {
"source": "knn_score",
"lang": "knn",
"params": {
"field": "embedding",
"query_value": [0.10, 0.20, 0.30, 0.40],
"space_type": "l2"
}
}
}
}
}'
This returns the truly nearest docs, computed exactly — useful as ground truth when
you're measuring the recall of the approximate path (the
recall/latency benchmark lab leans on this).
It's also the only k-NN you get on a knn_vector field that was indexed without a
method (a pure stored field). The script-score path is a ScriptPlugin contribution —
yet another extension interface KNNPlugin implements.
Bridge to source:
# The knn scoring script engine (ScriptPlugin contribution).
find src/main/java -path "*script*" -name "*.java" | head
grep -rn "knn_score\|KNNScoringSpace\|KNNScoringScriptEngine\|class KNNScoringUtil" \
src/main/java/org/opensearch/knn/plugin/script/ | head
Scenario 6: a semantic-search end-to-end (embeddings → vectors → results)
The scenarios above used toy 4-dim vectors so the geometry fit on a page. Here is the
real shape: text in, embeddings, a semantic query. We'll fake the embedding model with
a tiny deterministic stand-in so the example runs with zero external dependencies — in
production this float[] comes from a real model (via ml-commons/neural-search, or an
external service).
curl -s -XPUT 'localhost:9200/docs' -H 'Content-Type: application/json' -d '
{
"settings": { "index.knn": true },
"mappings": {
"properties": {
"text": { "type": "text" },
"embedding": { "type": "knn_vector", "dimension": 8, "space_type": "cosinesimil",
"method": { "name": "hnsw", "engine": "faiss" } }
}
}
}'
# In reality: emb = model.encode(text). Here we hand-place vectors so "cat" docs
# cluster and "tax" docs cluster, to show semantic retrieval working.
curl -s -H 'Content-Type: application/x-ndjson' \
-XPOST 'localhost:9200/docs/_bulk?refresh=true' --data-binary $'
{"index":{}}
{"text":"a small domestic feline","embedding":[0.9,0.1,0.1,0.0,0.1,0.0,0.0,0.1]}
{"index":{}}
{"text":"kitten care basics", "embedding":[0.88,0.12,0.08,0.0,0.1,0.0,0.0,0.1]}
{"index":{}}
{"text":"quarterly tax filing", "embedding":[0.0,0.0,0.1,0.9,0.0,0.8,0.1,0.0]}
'
# Query: embed "baby cat" -> a vector near the feline cluster -> nearest neighbors.
curl -s 'localhost:9200/docs/_search?pretty' -H 'Content-Type: application/json' -d '
{
"size": 2,
"_source": ["text"],
"query": { "knn": { "embedding": { "vector": [0.9,0.1,0.1,0.0,0.1,0.0,0.0,0.1], "k": 2 } } }
}'
The two feline docs come back; the tax doc does not — even though "baby cat" shares no
words with "a small domestic feline". That is the entire point of vector search: it
retrieves by meaning, where match retrieves by tokens. The natural next step is
hybrid search — combine a knn query with a match query in one request so you get
both semantic and lexical signal — which is one of the strongest reasons to run vectors
inside OpenSearch rather than in a standalone vector DB (recall the comparison in the
OpenSearch warm-up).
Scenario 7: where the memory actually lives — warmup and stats
This is the scenario that separates k-NN from every other field type, and the one most worth internalizing as a contributor. The faiss/nmslib graphs do not live on the JVM heap. They live in native memory (off-heap), loaded by JNI on first query against a segment — or eagerly via the warmup API.
# 1. Baseline: how much graph memory is loaded right now?
curl -s 'localhost:9200/_plugins/_knn/stats?pretty' \
| grep -E "graph_memory_usage|cache_capacity_reached|hit_count|miss_count|load_success_count"
# 2. Force-load every segment's graph for an index into native memory NOW,
# instead of paying the load latency on the first user query.
curl -s -XPOST 'localhost:9200/_plugins/_knn/warmup/cats?pretty'
# 3. Re-check stats: graph_memory_usage and load_success_count have moved.
curl -s 'localhost:9200/_plugins/_knn/stats?pretty' \
| grep -E "graph_memory_usage|load_success_count|hit_count|miss_count"
Before warmup, the first query against a cold segment pays a one-time cost: JNI reads the
graph file off disk into native memory and caches it. The warmup API pays that cost
up front so production queries are uniformly fast. The cache is managed by
NativeMemoryCacheManager (a guava cache), and a native-memory circuit breaker caps
the total — if loading another graph would blow the limit, the breaker trips and queries
fail rather than OOM-ing the box. You can watch the breaker in stats:
curl -s 'localhost:9200/_plugins/_knn/stats?pretty' | grep -iE "circuit_breaker|capacity"
What k-NN does under the hood:
- The warmup REST call (
RestKNNWarmupHandler) dispatches a transport action that, per shard, asksNativeMemoryCacheManagerto load each segment's graph allocation. NativeMemoryLoadStrategycalls JNI (JNIService→FaissService) tommap/read the graph file and hand back a native pointer wrapped in aNativeMemoryAllocation.- Every load is metered against the circuit breaker (
KNNCircuitBreaker, settingsknn.memory.circuit_breaker.limit/knn.memory.circuit_breaker.enabled). The rearchitecture of this guard is an active, real design discussion — k-NN #1582 — and a great window into off-heap engineering. - The stats you read come from
KNNStatssuppliers reading the cache manager and breaker.
Bridge to source:
# Warmup + stats REST handlers (ActionPlugin contributions)
find src/main/java -name "RestKNNWarmupHandler.java" -o -name "RestKNNStatsHandler.java"
# Native memory cache + load + circuit breaker
find src/main/java -name "NativeMemoryCacheManager.java" -o -name "KNNCircuitBreaker.java"
grep -rn "knn.memory.circuit_breaker" src/main/java/org/opensearch/knn/ | head
# The JNI boundary into native faiss
find src/main/java -path "*jni*" -name "JNIService.java" -o -name "FaissService.java"
See the native integration and memory chapter for the full off-heap story: the JNI boundary,
JNICommons,mmap, the circuit breaker's rearchitecture, and why a leak here is a native leak your JVM heap dump can't see.
The bridge table: user scenario → k-NN / Lucene class
Use this the way you'd use the master table in the
OpenSearch warm-up:
observe a behavior, find the code that owns it. Exact names drift by version — grep
under src/main/java/org/opensearch/knn to confirm.
| Observed behavior | Owning class / subsystem |
|---|---|
index.knn:true + knn_vector mapping accepted/validated | KNNVectorFieldMapper (+ .Builder), KNNVectorFieldType (MapperPlugin) |
method/engine/space_type validated against capabilities | KNNEngine, KNNMethod, MethodComponent, SpaceType |
| Float vs byte vs binary storage | VectorDataType (FLOAT/BYTE/BINARY) |
knn query parsed | KNNQueryBuilder → KNNQuery (SearchPlugin) |
| Per-shard nearest-neighbor scan + score conversion | KNNWeight, KNNScorer |
filter applied during traversal | KNNWeight filter handling; ENGINES_SUPPORTING_FILTERS |
max_distance / min_score radial search | KNNQueryBuilder radial branch; ENGINES_SUPPORTING_RADIAL_SEARCH |
Exact knn_score script | k-NN scoring script engine (ScriptPlugin), KNNScoringUtil |
| Graph written into segment files at flush/merge | KNN1030Codec → NativeEngines990KnnVectorsFormat (custom Lucene codec) |
Lucene engine vectors (.vec/.vex/.vem) | Lucene's KnnFloatVectorField + HNSW format — see Lucene HNSW |
| First-query graph load into native memory | NativeMemoryCacheManager, NativeMemoryLoadStrategy, JNIService → FaissService |
POST /_plugins/_knn/warmup/<index> | RestKNNWarmupHandler + warmup transport action |
GET /_plugins/_knn/stats | RestKNNStatsHandler, KNNStats suppliers |
| Off-heap memory cap trips | KNNCircuitBreaker (knn.memory.circuit_breaker.*) — k-NN #1582 |
_train model build (IVF/PQ) | TrainingModelTransportAction, ModelDao, .opensearch-knn-models (SystemIndexPlugin) |
Each row maps to a chapter: the field type and query path are detailed in
query path; engines and their capability sets in engines;
the algorithms behind method in algorithms; native memory
in native integration and memory; the codec in
architecture.
The contributor's loop for k-NN
The inner loop is the core loop
plus one wrinkle: the native build. k-NN is not pure Java, so a clean ./gradlew run
also compiles C++ under jni/.
cd ~/src/oss-repos/k-NN
# 1. Build the native libraries (faiss/nmslib wrappers via CMake) + the Java plugin,
# and run a node with the plugin installed. The first build is slow (it compiles
# faiss); later builds are incremental.
./gradlew run
# 2. Smoke test from another terminal.
curl -s 'localhost:9200/_cat/plugins?v' | grep -i knn
curl -s 'localhost:9200/_plugins/_knn/stats?pretty' | head
# 3. The fast inner loop: one Java test class.
./gradlew test --tests "org.opensearch.knn.index.query.KNNQueryBuilderTests"
# 4. Native-only build (when you're touching C++ under jni/).
cd jni && cmake . && make # or follow jni/README for the exact invocation
The build-from-source lab does this end to end,
including the submodule init for faiss/nmslib under jni/external/ and the platform
gotchas. If ./gradlew run fails on the native step, that's a CMake/C++ problem, not a
Java one — a distinction you'll learn to make fast.
Warning: A k-NN build pulls native git submodules (
jni/external/faiss,jni/external/nmslib). A fresh clone withoutgit submodule update --init --recursivefails the native build with confusing CMake errors. This is the single most common "my k-NN won't build" cause.
What to verify before the architecture chapter
Run this once. It proves your environment, and — more importantly — that you can move between user behavior and source.
# Environment
curl -s 'localhost:9200/_cat/plugins?v' | grep -i knn # plugin present
curl -s 'localhost:9200/_plugins/_knn/stats?pretty' | head # stats answer
# Round-trip
curl -s -XPUT 'localhost:9200/verify' -H 'Content-Type: application/json' -d \
'{"settings":{"index.knn":true},"mappings":{"properties":{"v":{"type":"knn_vector","dimension":3,"space_type":"l2","method":{"name":"hnsw","engine":"faiss"}}}}}'
curl -s -XPOST 'localhost:9200/verify/_doc?refresh=true' -H 'Content-Type: application/json' -d '{"v":[1,2,3]}'
curl -s 'localhost:9200/verify/_search?pretty' -H 'Content-Type: application/json' -d \
'{"query":{"knn":{"v":{"vector":[1,2,3],"k":1}}}}'
curl -s -XPOST 'localhost:9200/_plugins/_knn/warmup/verify?pretty'
You are ready when, without notes, you can:
-
State the three things that make an index a vector index:
index.knn:true, aknn_vectorfield type, and amethod(ormodel_id). - Explain what an embedding is and why "find similar" becomes "find nearest."
-
Name the difference between approximate
knn(withk), radial (max_distance/min_score), and exact (knn_scorescript) search — and which engines support filtering and radial. - Say where a faiss/nmslib graph physically lives at query time (native memory, off-heap) and what the warmup API and circuit breaker do about it.
-
Trace one
knnquery from REST to source:KNNQueryBuilder→KNNQuery→KNNWeight/KNNScorer→ JNI → faiss. -
Map each of: the field type, the query, the codec, and the native load to the
KNNPluginextension interface that owns it (MapperPlugin,SearchPlugin,EnginePlugin,ActionPlugin).
If any box is unchecked, re-run the scenario it maps to. When they all check, you've done the hard part: the source is no longer alien.
Continue to the k-NN Plugin Architecture to see how KNNPlugin
wires all of this together — the extension interfaces, the module layout, the codec, the
native memory manager — then to Engines for the faiss/lucene/nmslib
comparison. The labs (K1: build from source,
K2: trace a query) turn this reading into muscle
memory. And so long, and thanks for all the vectors.