Vector Search in OpenSearch

BackendSearchOpenSearchAI
Share on LinkedIn

Keyword search fails when users describe concepts differently than your documents. "How to reset my password" won't match a doc titled "Credential recovery procedure" with pure BM25. Vector search embeds both query and documents into a shared semantic space and retrieves by cosine similarity. OpenSearch ships native k-NN support with HNSW indexes — no separate vector database required if you're already on the OpenSearch stack.

Index setup with k-NN

PUT /documents
{
  "settings": {
    "index": {
      "knn": true,
      "knn.algo_param.ef_search": 100
    }
  },
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "content": { "type": "text" },
      "embedding": {
        "type": "knn_vector",
        "dimension": 384,
        "method": {
          "name": "hnsw",
          "space_type": "cosinesimil",
          "engine": "nmslib",
          "parameters": {
            "ef_construction": 128,
            "m": 16
          }
        }
      },
      "category": { "type": "keyword" },
      "tenant_id": { "type": "keyword" }
    }
  }
}

Match dimension to your embedding model output — 384 for MiniLM, 1536 for OpenAI ada-002.

Ingesting with embeddings

import { pipeline } from '@xenova/transformers';

const embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');

async function indexDocument(doc: { title: string; content: string; category: string }) {
  const text = `${doc.title}\n${doc.content}`;
  const output = await embedder(text, { pooling: 'mean', normalize: true });
  const embedding = Array.from(output.data as Float32Array);

  await opensearch.index({
    index: 'documents',
    body: {
      title: doc.title,
      content: doc.content,
      category: doc.category,
      embedding,
    },
  });
}

Batch embedding generation offline for large corpora; don't embed at query-time for ingestion.

Pure vector query

GET /documents/_search
{
  "size": 10,
  "query": {
    "knn": {
      "embedding": {
        "vector": [0.023, -0.041, "... 384 dims ..."],
        "k": 10
      }
    }
  }
}

Pre-filter with metadata:

{
  "query": {
    "bool": {
      "must": [
        {
          "knn": {
            "embedding": {
              "vector": [...],
              "k": 20
            }
          }
        }
      ],
      "filter": [
        { "term": { "tenant_id": "acme" }},
        { "term": { "category": "support" }}
      ]
    }
  }
}

Always filter tenant_id in multi-tenant setups — k-NN without filters leaks cross-tenant results.

Hybrid search (BM25 + vector)

OpenSearch 2.x hybrid query:

GET /documents/_search
{
  "query": {
    "hybrid": {
      "queries": [
        {
          "match": {
            "content": {
              "query": "password reset login",
              "boost": 0.3
            }
          }
        },
        {
          "knn": {
            "embedding": {
              "vector": [...],
              "k": 20,
              "boost": 0.7
            }
          }
        }
      ]
    }
  }
}

Tune BM25 vs vector boost weights against your golden query set. Support queries often need higher BM25 weight (exact error codes); exploratory queries need higher vector weight.

RAG retrieval pipeline

User query → embed query → hybrid search (top-k) → rerank → LLM context

Retrieve 20–50 candidates, rerank with a cross-encoder (Cohere rerank, bge-reranker) to top 5, inject into LLM prompt. Vector search is recall-oriented; reranking is precision-oriented.

Operational considerations

Concern Guidance
Index size 384-dim float32 = ~1.5KB/doc vector overhead
Reindex on model change New model = new embeddings = full reindex
Recall vs latency Increase ef_search for better recall
Freshness Near-real-time indexing default 1s refresh

Monitor p95 query latency and recall@10 on labeled sets when tuning HNSW parameters.

Choosing an embedding model

Model choice affects recall, latency, and index size:

Model Dimensions Size Best for
all-MiniLM-L6-v2 384 Small General text, fast inference
all-mpnet-base-v2 768 Medium Higher quality, slower
OpenAI text-embedding-3-small 1536 API Best quality, vendor lock-in
Cohere embed-v3 1024 API Multilingual

Match index dimension to model output. Changing models requires full reindex — version your embedding model in index metadata:

{
  "settings": {
    "index.knn": true,
    "meta": { "embedding_model": "all-MiniLM-L6-v2", "embedding_version": "1" }
  }
}

Run dual indexes during model migration — query both, compare recall, cut over when new model wins on golden set.

Pre-filtering vs post-filtering

k-NN search with filters has two strategies:

Pre-filtering — apply metadata filters before ANN search. Faster when filters are selective (tenant_id on 0.1% of docs). Required for multi-tenant isolation.

Post-filtering — retrieve top-k vectors, then filter. Higher recall when filters are broad but may return fewer than k results after filtering.

{
  "query": {
    "knn": {
      "embedding": {
        "vector": [...],
        "k": 50,
        "filter": {
          "bool": {
            "must": [
              { "term": { "tenant_id": "acme" }},
              { "range": { "created_at": { "gte": "now-90d" }}}
            ]
          }
        }
      }
    }
  }
}

If post-filter returns too few results, increase k before filtering (retrieve 100, filter to 10).

Hybrid search tuning methodology

BM25 vs vector boost weights aren't guessable — tune systematically:

  1. Build golden set with query, relevant doc IDs, and query type (keyword-heavy vs semantic)
  2. Run grid search: BM25 weight 0.0–1.0 in 0.1 increments, vector weight = 1 - BM25
  3. Measure recall@10 and MRR per query type
  4. Consider query-type routing: SKU queries → BM25-heavy; natural language → vector-heavy
# Pseudocode: query-type routing
def search(query: str):
    if re.match(r'^[A-Z]{2,}-\d+$', query):
        return bm25_search(query, boost=0.9)
    if len(query.split()) <= 2:
        return hybrid_search(query, bm25_weight=0.6)
    return hybrid_search(query, bm25_weight=0.3)

RAG-specific considerations

Vector search for RAG differs from user-facing search:

Index lifecycle and reindexing

Vector indexes are expensive to rebuild. Plan for:

Failure modes

Production checklist

Resources

Frequently asked questions

What is vector search in OpenSearch?

Vector search finds documents by embedding similarity rather than keyword matching. Text is converted to a dense vector (e.g., 768 dimensions from a model like all-MiniLM-L6-v2), stored in a k-NN index, and queried with approximate nearest neighbor (ANN) algorithms like HNSW. Semantically similar content ranks high even without shared keywords.

When should I combine vector search with BM25?

Hybrid search merges keyword relevance (BM25) with semantic similarity (vector). Use hybrid when queries contain exact identifiers (SKUs, error codes) that embeddings miss, but also need semantic matching ('fix login bug' matching 'authentication failure resolution'). Pure vector search alone misses exact token matches.

What HNSW parameters affect recall and latency?

m (max connections per node) and ef_construction (build-time search depth) control index quality vs build time. ef_search (query-time) trades latency for recall — higher ef_search finds more true neighbors but slows queries. Start with m=16, ef_construction=128, ef_search=100 and tune from there.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →