Ga naar hoofdinhoud

Hybrid Search: Combining Vector and Keyword Queries in MongoDB

Learn about hybrid search and how to utilize it in MongoDB.
· 6 min lezen

Verken met AI

Soms is eenvoudige full‑text search of alleen vector search niet genoeg om een database goed te bevragen en precies de resultaten te krijgen die je zoekt. De combinatie van beide is fantastisch wanneer een developer werkt met grote hoeveelheden multimodale, ongestructureerde data die van beide zoektypen profiteren. Dit staat bekend als hybride zoeken en biedt developers een uitstekende oplossing voor een lastige uitdaging. 

Wat is hybride zoeken precies? 

Om hybride zoeken goed te begrijpen, moeten we eerst begrijpen wat full‑text search is. 

Full‑text search is een manier van zoeken die letterlijke termen uit je query matcht met je documenten. Dit type traditionele zoekopdracht is waar veel developers juist heel vertrouwd mee zijn.

Als je bijvoorbeeld zoekt op “cute cafe with outdoor seating”, zal je zoekmachine naar die exacte woorden in de database zoeken. Simpel gezegd: full‑text search is ontzettend precies en efficiënt, maar werkt minder goed als je dezelfde resultaten wil bereiken met synoniemen, parafraseringen of zelfs met een typefout in je query. 

Vector search daarentegen zet alle data om in getallen, ofwel embeddings. In plaats van exacte woorden te matchen, vergelijkt vector search dus de semantische betekenis van je query met de documenten die in je database zijn opgeslagen. 

Zo kan zoeken op “cute cafe with outdoor seating” resultaten opleveren als “pastries and coffee outside”, ook al staan niet exact dezelfde woorden erin. Vector search is niet alleen semantisch; het is ook zeer flexibel, maar kan soms te brede resultaten teruggeven op basis van de opgegeven query. 

Waar komt hybride zoeken dan om de hoek kijken? Het combineert zowel full‑text search als vector search. Dit betekent dat developers niet alleen de semantische intelligentie van vectors kunnen benutten, maar ook de zeer precieze filtermogelijkheden van full‑text search behouden. Het is dus echt het beste van twee werelden. Dit is superhandig voor developers die werken met grote ongestructureerde datasets. 

Waarom hybride zoeken ertoe doet 

Hybride zoeken is nuttig in allerlei praktische toepassingen, waaronder (maar niet beperkt tot) e‑commerce, healthcare en zelfs recruitment. 

Neem e‑commerce: stel je voor dat je op je favoriete website zoekt naar “comfortable desk chair under $100”. Met vector search zie je semantisch vergelijkbare items (zoals ergonomische bureaustoelen), terwijl full‑text search focust op het woord “chair” en helpt om het prijsplafond af te dwingen. 

Bij recruitment: als een recruiter in cv’s zoekt naar “engineer with NLP experience”, weet hybride zoeken zowel “natural language processing” als het exacte trefwoord “engineer” te vangen.

Dit betekent dat je relevantere en betrouwbaardere resultaten krijgt dan met elk van beide aanpakken afzonderlijk. 

Hybride zoeken in MongoDB

Laten we doornemen hoe je hybride zoeken in MongoDB Atlas uitvoert. Om met deze tutorial aan de slag te gaan, heb je een paar dingen nodig: 

  1. Een MongoDB Atlas‑account  
  2. Een free‑tier MongoDB Atlas‑cluster
  3. mongosh geïnstalleerd in je terminal

In deze tutorial focussen we op de embedded movies‑collectie in onze sample_mflix‑database, en we volgen deze $rankFusion‑tutorial voor hybride zoeken, met een paar kanttekeningen. 

Hoewel de nieuwe $rankFusion-operator beschikbaar is in clusters met versie 8.1+, en hybride zoeken in MongoDB extreem vereenvoudigt, is deze nog in Public Preview en free‑tier clusters staan automatisch op versie 8.0. Dus we volgen deze tutorial, passen een paar dingen aan en ronden sterk af.

Voorzie je cluster

Zorg er bij het aanmaken van je cluster voor dat je de dataset sample_mflix downloadt. Dit is de dataset waar we in de hele tutorial mee werken. We focussen op de collectie sample_mflix.embedded_movies. 

Controleer snel of je mongosh hebt geïnstalleerd.

mongosh --version

Voor deze tutorial gebruik ik 2.4.2. 

Maak nu verbinding met je MongoDB Atlas‑cluster:

mongosh "mongodb+srv://<yourclusterhere>.mongodb.net/" --apiVersion 1 --username <yourusername> 

Je wordt gevraagd om het wachtwoord, en zodra je verbonden bent, ziet het er zo uit:

Nu kunnen we verbinding maken met onze database. Voer het commando uit:

use sample_mflix

De output zal zijn: 

switched to db sample_mflix

Maak je indexen aan

We kunnen nu beide indexen aanmaken die we voor deze tutorial nodig hebben. Dat zijn onze vectorindex en onze full‑text search‑index. Houd er rekening mee dat we beide indexen aanmaken op onze collectie embedded_movies. 

Vectorindex:

db.embedded_movies.createSearchIndex(
  "hybrid-vector-search",
  "vectorSearch",
  {
    fields: [
      { type: "vector", path: "plot_embedding_voyage_3_large", numDimensions: 2048, similarity: "dotProduct" }
    ]
  }
)

Full‑text search‑index:

db.embedded_movies.createSearchIndex(
  "hybrid-full-text-search",
  "search",
  { mappings: { dynamic: true } }
)

Controleer in je MongoDB Atlas‑cluster of de indexen klaar zijn:

Query uitvoeren op onze database

We willen nu de data in onze collectie sample_mflix.embedded_movies bevragen op “star wars” in het veld plot_embedding_voyage_3_large. 

Volgens de tutorial, omdat we geen API’s hoeven te gebruiken, kunnen we alle embeddings die we nodig hebben opslaan in een apart bestand met de naam query_embeddings.js. 

We kunnen nu onze embeddings laden om in de query te gebruiken. Voer het volgende uit:

load('/Users/<PATH NAME>/query_embeddings.js')
const queryVec = STAR_WARS_EMBEDDING

Om te controleren of de embeddings succesvol geladen zijn, voer je het commando uit:

STAR_WARS_EMBEDDING.length

Een output van 2048 is correct. 

Aggregatiepijplijn

We kunnen nu een aggregatiepijplijn maken voor hybride zoeken. Houd er rekening mee dat er een paar beperkingen en redenen zijn waarom we de aggregatiepijplijn op deze manier benaderen. 

Zoals hierboven gezegd: hoewel er $rankFusion in MongoDB Atlas is, bevindt deze zich momenteel in Public Preview, en heb je een cluster 8.0 of hoger nodig. Met een free‑tier cluster kun je $rankFusion op dit moment niet gebruiken. Dat maakt het wat ingewikkelder, omdat MongoDB Atlas de tekst‑ en vectorrangschikkingen niet native voor ons samenvoegt. Dit betekent dat we zelf moeten fuseren, ofwel een gewogen mix maken van de twee gerangschikte lijsten. 

De plaatsing van stages in aggregatiepijplijnen is ook strikt:

  • $search moet de eerste stage van een pijplijn zijn. 
  • $vectorSearch moet het begin van een pijplijn zijn en kan niet binnen $facet staan.

Een workaround? $vectorSearch kan de eerste stage zijn van onze subpijplijn binnen $unionWith, omdat deze subpijplijn wordt gezien als een eigen pijplijn. 

Daarom moeten we beginnen met $search voor onze full‑text search‑resultaten om hun score te behouden, en vervolgens $unionWith gebruiken als een tweede mini‑pijplijn die start met $vectorSearch zodat ook die score behouden blijft. 

Daarna moeten we groeperen op _id zodat we geen duplicaten hebben en zodat we het beste gebruiken uit elk van onze $search- en $vectorSearch-termen. 

Als dat gedaan is, moeten we de hybride zoekscore bepalen: het beste uit onze $search, het beste uit onze $vectorSearch, vervolgens vermenigvuldigd met een gewicht naar keuze (ik koos 0,35), en van daaruit sorteren. 

Het gewicht bepaalt hoeveel invloed vector search heeft ten opzichte van full‑text search in de eindresultaten. 

Onze hybride formule is: hybrid_score = text_score + (vector_score x weight). Een hoger gewicht (bijv. 0,7–1,0) betekent dat vector search domineert en dus semantische gelijkenis prioriteert. 

Een lager gewicht (0,1–0,3) betekent daarentegen dat full‑text search domineert en exacte keyword‑matches prioriteert. 

Kopieer en plak deze aggregatiepijplijn in je terminalvenster:

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()

Hier is onze output:

  {
    _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
  }
]).toArray()

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.

Onderwerpen

Gerelateerd