Full-text search finds documents that contain the words you typed. Semantic search finds documents that mean what you meant.
Neither is sufficient alone. A keyword search for "memory consolidation" misses articles that discuss the same concept under "replay buffer" or "episodic compression." A pure vector search finds topically adjacent documents but can miss exact phrase matches that matter in technical writing.
The right answer is hybrid: run both, then merge the ranked lists.
The Client
The OpenSearch client exposes three search modes: text, semantic, and hybrid.
examples/opensearch/client.tsBM25 Text Search
The textSearch function issues a multi_match query across four fields with different weights. The title field is boosted 3× - a match in the title is much stronger signal than a match in the body:
query: {
multi_match: {
query,
fields: ['title^3', 'description^2', 'content', 'tags'],
type: 'best_fields',
fuzziness: 'AUTO',
},
}Fuzziness handles typos. A query for "opensearh" finds "opensearch" with edit distance 1.
k-NN Vector Search
The semanticSearch function queries the content_embedding field using a pre-computed dense vector. OpenSearch's k-NN plugin (backed by FAISS or NMSLIB) finds the k nearest neighbors in the embedding space:
knn: {
content_embedding: {
vector: embedding, // float32[1536] from an embedding model
k: size,
},
}The filter clause narrows the k-NN search to specific tags - useful when searching within a series or topic area.
Reciprocal Rank Fusion
Merging two ranked lists is a solved problem. Reciprocal rank fusion (RRF) assigns a score of 1 / (k + rank) to each result, where k=60 dampens the impact of low-rank results. Scores from both lists are summed:
textResults.forEach((hit, rank) => {
scores.set(hit.slug, (scores.get(hit.slug) ?? 0) + 1 / (k + rank + 1))
})
vectorResults.forEach((hit, rank) => {
scores.set(hit.slug, (scores.get(hit.slug) ?? 0) + 1 / (k + rank + 1))
})Documents appearing in both lists rank higher than documents appearing in only one. This is the same technique used in production hybrid search systems.
Index Design
The blog post index maps look like this:
{
"mappings": {
"properties": {
"slug": { "type": "keyword" },
"title": { "type": "text", "analyzer": "english" },
"description": { "type": "text", "analyzer": "english" },
"content": { "type": "text", "analyzer": "english" },
"tags": { "type": "keyword" },
"content_embedding": {
"type": "knn_vector",
"dimension": 1536,
"method": { "name": "hnsw", "engine": "faiss" }
}
}
}
}The HNSW index (Hierarchical Navigable Small World) gives sub-linear k-NN search at query time, with a build-time tradeoff between index size and recall.