Home / AI & GenAI / Vector Databases

AI & GenAICheat sheetIntermediate

OpenSearch k-NN cheat sheet: vector mappings, bulk loads, filtered and hybrid queries, aliases and health checks

The OpenSearch calls you need to run a RAG vector index - knn_vector mappings, fast bulk loads, filtered k-NN and hybrid queries, alias swaps and the health checks to run first.

Deepak Bhardwaj7 min read

Applies toOpenSearch 2.x and 3.x with the k-NN and neural-search plugins, self-managed and Amazon OpenSearch Service (feature availability varies by version)

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.

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=1 reduces 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 errors on every _bulk response and compare _count against the source document count.
  • Alert on cluster status, graph_memory_usage_percentage and circuit breaker triggers.
  • Rerun a recall check on a golden set after every mapping, engine or embedding model change.