How to use this sheet
The calls below use curl against a self-managed cluster. On Amazon OpenSearch Service with IAM authentication, sign the requests with SigV4 (for example with awscurl --service es) or use the OpenSearch Dashboards Dev Tools console with the same paths and bodies.
export OS=https://os-prod:9200
alias oscurl='curl -s -u admin:$PW -H "Content-Type: application/json"'
Tip
Defaults and supported options change between k-NN plugin releases. The default engine, which engines support which space types, query-time parameters and disk-based mode all vary. Check the k-NN docs for the version you actually run (
GET /).
Cluster and plugin checks
oscurl "$OS/" # version.number
oscurl "$OS/_cat/plugins?v" | grep -E "knn|neural"
oscurl "$OS/_cluster/health?pretty"
oscurl "$OS/_cat/indices/rag-*?v&h=index,health,docs.count,store.size,pri,rep"
oscurl "$OS/_plugins/_knn/stats?pretty" | grep -E "graph_memory_usage_percentage|circuit_breaker_triggered|eviction_count"
If the cluster is red or the k-NN circuit breaker has tripped, stop here and use the Sev1 runbook for OpenSearch k-NN outages.
Create a vector index
oscurl -X PUT "$OS/rag-docs-v2" -d '{
"settings": {
"index": { "knn": true, "number_of_shards": 2, "number_of_replicas": 1 }
},
"mappings": {
"properties": {
"embedding": {
"type": "knn_vector",
"dimension": 1024,
"method": {
"name": "hnsw",
"engine": "faiss",
"space_type": "innerproduct",
"parameters": { "m": 16, "ef_construction": 128 }
}
},
"chunk_text": { "type": "text" },
"doc_id": { "type": "keyword" },
"tenant_id": { "type": "keyword" },
"updated_at": { "type": "date" }
}
}
}'
| Field | What to set | Gotcha |
|---|---|---|
index.knn | true | Without it, approximate search isn't available on the index |
dimension | Exactly your embedding model's output size | Can't be changed. A new model means a new index |
space_type | Match what the embedding model was trained for (cosinesimil, innerproduct, l2) | innerproduct on normalized vectors gives the same ranking as cosine. Engine support for each space type varies by version |
engine | faiss or lucene (nmslib is deprecated in newer releases) | Engine and method can't be changed after creation |
m, ef_construction | Start at 16 and 100–128 | Higher values give better recall but more memory and slower builds |
Check what you actually got:
oscurl "$OS/rag-docs-v2/_mapping?pretty"
oscurl "$OS/rag-docs-v2/_settings?pretty&include_defaults=true" | grep -E "knn|refresh_interval|replicas"
Lower memory: disk-based mode (newer versions)
In releases that support it, "mode": "on_disk" on the knn_vector field keeps compressed vectors in memory and rescores with full-precision vectors from disk. compression_level (for example "32x") sets the trade-off. It supports float vectors only. Test recall before you use it in production.
Bulk load fast
Before a full load, turn off refresh and replicas:
oscurl -X PUT "$OS/rag-docs-v2/_settings" -d '{"index":{"refresh_interval":"-1","number_of_replicas":0}}'
NDJSON bulk file, two lines per document, and the file must end with a newline:
{"index":{"_index":"rag-docs-v2","_id":"kb-1042#3"}}
{"embedding":[0.012,-0.044,...],"chunk_text":"To reset MFA...","doc_id":"kb-1042","tenant_id":"acme","updated_at":"2026-11-01"}
oscurl -X POST "$OS/_bulk" --data-binary @batch-0001.ndjson | jq '{errors, failed: [.items[] | select(.index.error) | .index.error.reason][0:5]}'
Always check errors. _bulk returns HTTP 200 even when individual documents fail, for example because of a dimension mismatch.
When the load has finished, restore the settings and refresh:
oscurl -X PUT "$OS/rag-docs-v2/_settings" -d '{"index":{"refresh_interval":"1s","number_of_replicas":1}}'
oscurl -X POST "$OS/rag-docs-v2/_refresh"
oscurl "$OS/rag-docs-v2/_count"
Warning
With replicas at 0, losing a node during the load loses data. That's fine for a rebuildable index that isn't live yet. Never do it on the index the alias points to.
Use stable IDs such as doc_id#chunk, so that re-running a batch overwrites documents instead of creating duplicates.
Query
Plain k-NN. Leave the vector out of the response, because it is large:
oscurl "$OS/rag-docs/_search" -d '{
"size": 5,
"_source": { "excludes": ["embedding"] },
"query": { "knn": { "embedding": { "vector": [0.01, -0.03, ...], "k": 20 } } }
}'
k is the number of neighbours fetched per shard or segment, and size is the number of results returned. Set k to at least size.
Filtered k-NN (tenant, ACL, freshness)
Put the filter inside the knn clause. Then the engine filters during the search. If you wrap the query in a bool with a post-filter instead, you can end up with fewer than size results.
oscurl "$OS/rag-docs/_search" -d '{
"size": 5,
"_source": { "excludes": ["embedding"] },
"query": {
"knn": {
"embedding": {
"vector": [0.01, -0.03, ...],
"k": 20,
"filter": {
"bool": { "filter": [
{ "term": { "tenant_id": "acme" } },
{ "range": { "updated_at": { "gte": "now-365d" } } }
] }
}
}
}
}
}'
Efficient filtering inside knn is supported for the lucene engine and, in later 2.x releases, for faiss. Check your version.
Query-time ef_search
In versions that support method_parameters, raise recall for one query without changing the index:
"knn": { "embedding": { "vector": [...], "k": 20, "method_parameters": { "ef_search": 200 } } }
On older versions, set it at index level with index.knn.algo_param.ef_search (this applies to the faiss and nmslib engines).
Hybrid (BM25 + vector)
Create a search pipeline that normalizes and combines the scores (neural-search plugin, 2.10 and later):
oscurl -X PUT "$OS/_search/pipeline/rag-hybrid" -d '{
"phase_results_processors": [ { "normalization-processor": {
"normalization": { "technique": "min_max" },
"combination": { "technique": "arithmetic_mean", "parameters": { "weights": [0.3, 0.7] } }
} } ]
}'
oscurl "$OS/rag-docs/_search?search_pipeline=rag-hybrid" -d '{
"size": 5,
"_source": { "excludes": ["embedding"] },
"query": { "hybrid": { "queries": [
{ "match": { "chunk_text": "reset MFA for a contractor" } },
{ "knn": { "embedding": { "vector": [0.01, -0.03, ...], "k": 20 } } }
] } }
}'
The weights follow the order of queries (here 0.3 BM25, 0.7 vector). Tune them against a golden question set, not by eye.
Aliases: switch index versions without downtime
The application always queries rag-docs, never a versioned index name.
oscurl "$OS/_cat/aliases/rag-docs?v"
oscurl -X POST "$OS/_aliases" -d '{ "actions": [
{ "remove": { "index": "rag-docs-v1", "alias": "rag-docs" } },
{ "add": { "index": "rag-docs-v2", "alias": "rag-docs" } }
] }'
Both actions apply atomically. To roll back, run the same call with the indexes swapped. Keep v1 until v2 has run cleanly for a while, then delete it. Two loaded versions double the graph memory.
Housekeeping
oscurl "$OS/_plugins/_knn/warmup/rag-docs-v2?pretty" # preload graphs after restart or alias swap
oscurl "$OS/_cat/segments/rag-docs-v2?v&h=index,shard,segment,docs.count,size" | wc -l
oscurl -X POST "$OS/rag-docs-v2/_delete_by_query" -d '{"query":{"term":{"doc_id":"kb-0999"}}}'
Caution
POST /<index>/_forcemerge?max_num_segments=1reduces segment count and speeds up k-NN search. On a large index it is heavy on I/O and CPU and can take hours. Run it on a new index before the alias swap, not on the live one at peak.
Common errors
| Error or symptom | Cause | Fix |
|---|---|---|
| Bulk item error about vector dimension | Embedding size differs from the mapping dimension | Fix the model or config. Create a new index for the new size |
Query error: field is not a knn_vector | Index created without the mapping (dynamic mapping made it float) | Create the index with an explicit mapping first, then reindex |
Filtered query returns fewer than size hits | Filter applied after the search, or a very selective filter | Move the filter inside knn, raise k |
| First queries after a restart are slow | Graphs loading from disk | Warmup API |
search_pipeline not found | Pipeline missing on this cluster, or neural-search plugin not installed | Create the pipeline, check _cat/plugins |
Prevent it next time
- Keep the index definition (settings, mapping, pipeline) in source control and create indexes only from it.
- Version index names (
-v1,-v2) behind an alias from day one. - Check
errorson every_bulkresponse and compare_countagainst the source document count. - Alert on cluster status,
graph_memory_usage_percentageand circuit breaker triggers. - Rerun a recall check on a golden set after every mapping, engine or embedding model change.