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_type mapping parameter picks the distance metric: l2 (Euclidean), cosinesimil (cosine), innerproduct, l1, linf, and hamming (for binary vectors). Pick the one your embedding model was trained for — using l2 on 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:

  1. At mapping time, KNNVectorFieldMapper.Builder parses dimension, space_type, and the method object, validates them against the chosen KNNEngine (faiss), and produces a KNNVectorFieldType. This is a MapperPlugin contribution — the same extension point any custom field type uses (see plugin architecture).
  2. On index, each float[] is stored, and at flush/merge a custom Lucene codec (KNN1030Codec and its NativeEngines990KnnVectorsFormat, 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.
  3. The knn query parses into a KNNQueryBuilder, which builds a KNNQuery. Per shard, KNNWeight/KNNScorer ask the loaded faiss graph for the nearest k doc IDs and their distances, convert distance → score, and feed them into the normal Lucene collector. The coordinating node merges the per-shard top-k exactly 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:

  1. The warmup REST call (RestKNNWarmupHandler) dispatches a transport action that, per shard, asks NativeMemoryCacheManager to load each segment's graph allocation.
  2. NativeMemoryLoadStrategy calls JNI (JNIServiceFaissService) to mmap/read the graph file and hand back a native pointer wrapped in a NativeMemoryAllocation.
  3. Every load is metered against the circuit breaker (KNNCircuitBreaker, settings knn.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.
  4. The stats you read come from KNNStats suppliers 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 behaviorOwning class / subsystem
index.knn:true + knn_vector mapping accepted/validatedKNNVectorFieldMapper (+ .Builder), KNNVectorFieldType (MapperPlugin)
method/engine/space_type validated against capabilitiesKNNEngine, KNNMethod, MethodComponent, SpaceType
Float vs byte vs binary storageVectorDataType (FLOAT/BYTE/BINARY)
knn query parsedKNNQueryBuilderKNNQuery (SearchPlugin)
Per-shard nearest-neighbor scan + score conversionKNNWeight, KNNScorer
filter applied during traversalKNNWeight filter handling; ENGINES_SUPPORTING_FILTERS
max_distance / min_score radial searchKNNQueryBuilder radial branch; ENGINES_SUPPORTING_RADIAL_SEARCH
Exact knn_score scriptk-NN scoring script engine (ScriptPlugin), KNNScoringUtil
Graph written into segment files at flush/mergeKNN1030CodecNativeEngines990KnnVectorsFormat (custom Lucene codec)
Lucene engine vectors (.vec/.vex/.vem)Lucene's KnnFloatVectorField + HNSW format — see Lucene HNSW
First-query graph load into native memoryNativeMemoryCacheManager, NativeMemoryLoadStrategy, JNIServiceFaissService
POST /_plugins/_knn/warmup/<index>RestKNNWarmupHandler + warmup transport action
GET /_plugins/_knn/statsRestKNNStatsHandler, KNNStats suppliers
Off-heap memory cap tripsKNNCircuitBreaker (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 without git submodule update --init --recursive fails 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, a knn_vector field type, and a method (or model_id).
  • Explain what an embedding is and why "find similar" becomes "find nearest."
  • Name the difference between approximate knn (with k), radial (max_distance/ min_score), and exact (knn_score script) 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 knn query from REST to source: KNNQueryBuilderKNNQueryKNNWeight/KNNScorer → JNI → faiss.
  • Map each of: the field type, the query, the codec, and the native load to the KNNPlugin extension 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.