Curso
A veces, la búsqueda de texto completo por sí sola o solo la búsqueda vectorial no bastan para consultar correctamente una base de datos y obtener los resultados que buscas. La combinación de ambas es ideal cuando un desarrollador trabaja con grandes volúmenes de datos multimodales y no estructurados que se benefician de los dos tipos de búsqueda. A esto se le llama búsqueda híbrida, y ofrece a los desarrolladores una gran solución para un reto complejo.
¿Qué es exactamente la búsqueda híbrida?
Para entender bien la búsqueda híbrida, primero tenemos que comprender qué es la búsqueda de texto completo.
La búsqueda de texto completo compara términos literales de tu consulta con los documentos. Este enfoque tradicional es el que muchos desarrolladores conocen bien.
Por ejemplo, si buscas «cafetería mona con terraza», el motor buscará esas palabras exactas en la base de datos. En pocas palabras, la búsqueda de texto completo es muy precisa y eficiente, pero no funciona bien si quieres obtener los mismos resultados usando sinónimos, parafraseando o si tu consulta tiene una errata.
La búsqueda vectorial, en cambio, convierte todos los datos en números, o embeddings. En lugar de hacer coincidir palabras exactas, la búsqueda vectorial compara el significado semántico de tu consulta con los documentos almacenados en la base de datos.
Si buscas «cafetería mona con terraza», puede devolverte «pasteles y café al aire libre», aunque no use las mismas palabras. La búsqueda vectorial no solo es semántica; también es muy flexible, pero a veces puede devolver resultados demasiado amplios para la consulta indicada.
Entonces, ¿dónde entra la búsqueda híbrida? Combina la búsqueda de texto completo y la búsqueda vectorial. Así, los desarrolladores aprovechan la inteligencia semántica de los vectores y, al mismo tiempo, conservan los filtros precisos de la búsqueda de texto completo. Es, literalmente, lo mejor de ambos mundos. Y es especialmente útil al trabajar con grandes conjuntos de datos no estructurados.
Por qué importa la búsqueda híbrida
La búsqueda híbrida es útil en muchas aplicaciones reales, como comercio electrónico, salud o incluso selección de personal.
En e-commerce, imagina entrar en tu web favorita y buscar «silla de escritorio cómoda por menos de 100 $». Con la búsqueda vectorial verás artículos semánticamente similares (por ejemplo, sillas ergonómicas), mientras que la búsqueda de texto completo se centra en la palabra «silla» y ayuda a aplicar el límite de precio.
En selección, si una persona reclutadora busca en currículos «ingeniero con experiencia en NLP», la búsqueda híbrida capta tanto «natural language processing» como la palabra clave exacta «engineer».
Esto significa que obtendrás resultados más relevantes y fiables que usando cualquiera de los dos enfoques por separado.
Búsqueda híbrida en MongoDB
Veamos cómo hacer búsqueda híbrida en MongoDB Atlas. Para seguir este tutorial con éxito, necesitarás algunos requisitos previos:
- Una cuenta de MongoDB Atlas
- Un clúster de MongoDB Atlas de la capa gratuita
mongoshinstalado en tu terminal
En este tutorial nos centraremos en la colección de películas embebidas del conjunto de datos sample_mflix, y seguiremos este tutorial de búsqueda híbrida con $rankFusion, con algunos matices.
Aunque el nuevo operador $rankFusion está disponible en clústeres con la versión 8.1+ y facilita muchísimo la búsqueda híbrida en MongoDB, sigue en Public Preview y los clústeres de la capa gratuita están en la versión 8.0. Así que seguiremos este tutorial, ajustaremos un par de cosas y llegaremos al objetivo.
Provisiona tu clúster
Al crear tu clúster, asegúrate de descargar el conjunto de datos sample_mflix. Es el que usaremos durante todo el tutorial. Nos centraremos en la colección sample_mflix.embedded_movies.

Comprueba rápidamente que tienes mongosh instalado.
mongosh --version
Para este tutorial, estoy usando la 2.4.2.
Ahora, conéctate a tu clúster de MongoDB Atlas:
mongosh "mongodb+srv://<yourclusterhere>.mongodb.net/" --apiVersion 1 --username <yourusername>
Se te pedirá la contraseña y, una vez conectado, verás algo así:

Ahora podemos conectarnos a nuestra base de datos. Ejecuta el comando:
use sample_mflix
La salida será:
switched to db sample_mflix
Crea tus índices
Vamos a crear los dos índices que necesitamos para este tutorial: el índice vectorial y el índice de texto completo. Ten en cuenta que ambos índices los creamos en la colección embedded_movies.
Índice vectorial:
db.embedded_movies.createSearchIndex(
"hybrid-vector-search",
"vectorSearch",
{
fields: [
{ type: "vector", path: "plot_embedding_voyage_3_large", numDimensions: 2048, similarity: "dotProduct" }
]
}
)
Índice de texto completo:
db.embedded_movies.createSearchIndex(
"hybrid-full-text-search",
"search",
{ mappings: { dynamic: true } }
)
Verifica en tu clúster de MongoDB Atlas que los índices estén listos:

Consulta nuestra base de datos
Ahora queremos consultar los datos de sample_mflix.embedded_movies para «star wars» en el campo plot_embedding_voyage_3_large.
Siguiendo el tutorial, como no tenemos que usar APIs, podemos guardar todos los embeddings necesarios en un archivo aparte llamado query_embeddings.js.

Ahora podemos cargar los embeddings para usarlos en la consulta. Ejecuta lo siguiente:
load('/Users/<PATH NAME>/query_embeddings.js')
const queryVec = STAR_WARS_EMBEDDING
Para comprobar que los embeddings se han cargado correctamente, ejecuta:
STAR_WARS_EMBEDDING.length

Una salida de 2048 es correcta.
Pipeline de agregación
Ahora podemos crear un pipeline de agregación para la búsqueda híbrida. Ten en cuenta que hay algunas limitaciones y razones por las que abordamos el pipeline de esta manera.
Como hemos dicho, aunque $rankFusion está en MongoDB Atlas, actualmente está en Public Preview y necesitas un clúster 8.0 o superior. Con la capa gratuita no puedes usar $rankFusion por ahora. Esto complica un poco las cosas porque Atlas no fusionará de forma nativa las clasificaciones de texto y vector. Así que debemos hacer nuestra propia fusión, o una mezcla ponderada de ambas listas ordenadas.
La colocación de etapas en los pipelines de agregación también es estricta:
$searchdebe ser la primera etapa del pipeline.$vectorSearchtiene que ir al inicio de un pipeline y no puede estar dentro de$facet.
¿La solución? $vectorSearch puede ser la primera etapa de un subpipeline dentro de $unionWith, ya que ese subpipeline se considera un pipeline independiente.
Por ello, debemos empezar con $search para que los resultados de texto completo conserven su puntuación, y usar $unionWith como un mini-pipeline que comienza con $vectorSearch para que también conserve su puntuación.
Luego necesitamos agrupar por _id para evitar duplicados y usar lo mejor de $search y $vectorSearch.
Después, calculamos la búsqueda híbrida: lo mejor de $search, lo mejor de $vectorSearch, multiplicado por un peso a elegir (yo he elegido 0.35) y ordenamos a partir de ahí.
El peso determina cuánto influye la búsqueda vectorial frente a la búsqueda de texto completo en los resultados finales.
Nuestra fórmula híbrida es: hybrid_score = text_score + (vector_score x weight). Un peso alto (p. ej., 0.7-1.0) hace que predomine la búsqueda vectorial y, por tanto, la similitud semántica.
Un peso bajo (0.1-0.3), en cambio, hace que predomine la búsqueda de texto completo y prioriza las coincidencias exactas de palabras clave.
Copia y pega este pipeline de agregación en tu terminal:
const WEIGHT = 0.35;
db.embedded_movies.aggregate([
// Stage 1: full-text search
{ $search: { index: "hybrid-full-text-search", text: { query: "star wars", path: ["title","plot"] } } },
{ $set: { t: { $meta: "searchScore" } } },
{ $project: { _id: 1, title: 1, plot: 1, t: 1 } },
// Stage 2: Union with vector search
{ $unionWith: {
coll: "embedded_movies",
pipeline: [
// Vector search subpipeline
{ $vectorSearch: {
index: "hybrid-vector-search",
path: "plot_embedding_voyage_3_large",
queryVector: queryVec,
numCandidates: 200,
limit: 150
}},
{ $project: { _id: 1, title: 1, plot: 1, v: { $meta: "vectorSearchScore" } } }
]
}},
// Stage 3: Combine and rank results
{ $group: { _id: "$_id", title: { $first: "$title" }, plot: { $first: "$plot" }, t: { $max: "$t" }, v: { $max: "$v" } } },
{ $set: { h: { $add: [ { $ifNull: ["$t", 0] }, { $multiply: [ { $ifNull: ["$v", 0] }, WEIGHT ] } ] } } },
{ $sort: { h: -1 } },
{ $limit: 20 }
]).toArray()
Este es el resultado:
{
_id: ObjectId('573a139af29313caabcf124d'),
title: 'Star Wars: Episode III - Revenge of the Sith',
plot: 'As the Clone Wars near an end, the Sith Lord Darth Sidious steps out of the shadows, at which time Anakin succumbs to his emotions, becoming Darth Vader and putting his relationships with Obi-Wan and Padme at risk.',
t: 4.957413673400879,
v: 0.756283700466156,
h: 5.2221129685640335
},
{
_id: ObjectId('573a13a6f29313caabd17d08'),
title: 'Star',
plot: 'The Driver now carries an arrogant rock star who is visiting a major city (not Pittsburgh as earlier believed). Played by Madonna, this title character wants to get away from her bodyguards...',
t: 5.151784420013428,
v: null,
h: 5.151784420013428
},
{
_id: ObjectId('573a1397f29313caabce8cdb'),
title: 'Star Wars: Episode VI - Return of the Jedi',
plot: 'After rescuing Han Solo from the palace of Jabba the Hutt, the rebels attempt to destroy the second Death Star, while Luke struggles to make Vader return from the dark side of the Force.',
t: 4.726564407348633,
v: 0.7782549858093262,
h: 4.998953652381897
},
{
_id: ObjectId('573a13aef29313caabd2da15'),
title: 'Star Runner',
plot: 'Get ready for the ultimate martial arts competition, where anything goes and lives are bought and sold. Tank is the celebrated Champion Star Runner and is deemed invincible among the ...',
t: 4.702453136444092,
v: 0.696668267250061,
h: 4.9462870299816135
},
{
_id: ObjectId('573a13c0f29313caabd62f62'),
title: 'Star Wars: The Clone Wars',
plot: 'Anakin Skywalker and Ahsoka Tano must rescue the kidnapped son of Jabba the Hutt, but political intrigue complicates their mission.',
t: 4.537533760070801,
v: 0.7561323642730713,
h: 4.802180087566375
},
{
_id: ObjectId('573a1397f29313caabce6f53'),
title: 'Message from Space',
plot: 'In this Star Wars take-off, the peaceful planet of Jillucia has been nearly wiped out by the Gavanas, whose leader takes orders from his mother (played a comic actor in drag) rather than ...',
t: 4.401333808898926,
v: 0.7913960218429565,
h: 4.678322416543961
},
{
_id: ObjectId('573a139df29313caabcfa90b'),
title: 'Message from Space',
plot: 'In this Star Wars take-off, the peaceful planet of Jillucia has been nearly wiped out by the Gavanas, whose leader takes orders from his mother (played a comic actor in drag) rather than ...',
t: 4.401333808898926,
v: 0.7913960218429565,
h: 4.678322416543961
},
{
_id: ObjectId('573a1398f29313caabce9851'),
title: 'Gymkata',
plot: 'Johnathan Cabot is a champion gymnast. In the tiny, yet savage, country of Parmistan, there is a perfect spot for a "star wars" site. For the US to get this site, they must compete in the ...',
t: 4.284864902496338,
v: 0.7193170189857483,
h: 4.53662585914135
},
{
_id: ObjectId('573a1397f29313caabce68f6'),
title: 'Star Wars: Episode IV - A New Hope',
plot: "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a wookiee and two droids to save the universe from the Empire's world-destroying battle-station, while also attempting to rescue Princess Leia from the evil Darth Vader.",
t: 2.9629626274108887,
v: 0.7987099885940552,
h: 3.242511123418808
},
{
_id: ObjectId('573a139af29313caabcf0f5f'),
title: 'Star Wars: Episode I - The Phantom Menace',
plot: 'Two Jedi Knights escape a hostile blockade to find allies and come across a young boy who may bring balance to the Force, but the long dormant Sith resurface to reclaim their old glory.',
t: 2.9629626274108887,
v: 0.7771433591842651,
h: 3.2349628031253816
},
{
_id: ObjectId('573a1397f29313caabce77d9'),
title: 'Star Wars: Episode V - The Empire Strikes Back',
plot: 'After the rebels have been brutally overpowered by the Empire on their newly established base, Luke Skywalker takes advanced Jedi training with Master Yoda, while his friends are pursued by Darth Vader as part of his plan to capture Luke.',
t: 2.7174296379089355,
v: 0.7810181975364685,
h: 2.9907860070466996
},
{
_id: ObjectId('573a139af29313caabcf1258'),
title: 'Star Wars: Episode II - Attack of the Clones',
plot: 'Ten years after initially meeting, Anakin Skywalker shares a forbidden romance with Padmè, while Obi-Wan investigates an assassination attempt on the Senator and discovers a secret clone army crafted for the Jedi.',
t: 2.7174296379089355,
v: 0.7400004267692566,
h: 2.9764297872781755
},
{
_id: ObjectId('573a1394f29313caabcdf65b'),
title: 'Ugetsu',
plot: 'A fantastic tale of war, love, family and ambition set in the midst of the Japanese Civil Wars of the sixteenth century.',
t: 2.858368396759033,
v: null,
h: 2.858368396759033
},
{
_id: ObjectId('573a13a4f29313caabd1137f'),
title: 'S1m0ne',
plot: "A producer's film is endangered when his star walks off, so he decides to digitally create an actress to substitute for the star, becoming an overnight sensation that everyone thinks is a real person.",
t: 2.842216730117798,
v: null,
h: 2.842216730117798
},
{
_id: ObjectId('573a13b8f29313caabd4c3c3'),
title: 'Star Trek',
plot: "The brash James T. Kirk tries to live up to his father's legacy with Mr. Spock keeping him in check as a vengeful, time-traveling Romulan creates black holes to destroy the Federation one planet at a time.",
t: 2.577816963195801,
v: 0.7295459508895874,
h: 2.8331580460071564
},
{
_id: ObjectId('573a13b5f29313caabd42e99'),
title: 'Sars Wars',
plot: "The fourth generation of the virus SARS is found in Africa! It's more dangerous and causes the patients to transform into bloodthirsty zombies. The virus quickly lands to Thailand, Dr. ...",
t: 2.826810598373413,
v: null,
h: 2.826810598373413
},
{
_id: ObjectId('573a13b5f29313caabd42e1b'),
title: 'Sars Wars',
plot: "The fourth generation of the virus SARS is found in Africa! It's more dangerous and causes the patients to transform into bloodthirsty zombies. The virus quickly lands to Thailand, Dr. ...",
t: 2.826810598373413,
v: null,
h: 2.826810598373413
}
As we can see, we have some results that are identical to our query, “star wars,” and other results that are clearly off of meaning.
Conclusion
Congratulations! You have successfully completed hybrid search in MongoDB. While $rankFusion will simplify this process, this method shows a workaround while the operator is still in Public Preview. Through this tutorial, we have successfully incorporated both full-text search and vector search into one pipeline to retrieve the most optimal results for our given query. For more information on hybrid search in MongoDB, please refer to the MongoDB documentation. If you’re still getting up to speed with MongoDB, I recommend the Introduction to MongoDB in Python course.
MongoDB Hybrid Search FAQs
Do I need MongoDB 8.1 and `$rankFusion` to do hybrid search
No. On 8.0, you can actually fuse the scores yourself. Do this by running $search and $vectorSearch in two pipelines and combine them with $unionWith + $group and a weighted formula. On 8.1+, $rankFusion will do this for you.
What is hybrid search?
It’s combining full-text search (exact words) and vector search (meaning) for the most optimal results possible from a given query.
When should I use hybrid search?
Use hybrid search when your text is varied (synonyms, paraphrases, typos) but you still want exact terms.
How do I pick the weight I want to use?
It’s best practice to begin around 0.2-0.5. The lower is more for full-text search influence, and higher is for semantic influence. It’s important to tune the weight after testing and viewing the results provided.
Can hybrid search work with image and audio data?
Yes! As long as your data can be turned into vector embeddings and you can combine them with any specific wording constraints, you can perform hybrid search on a dataset.

