🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 22 Min Lesezeit
0

Building a Vibe-Based Music Recommender with MongoDB and Voyage AI

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Have you ever found yourself in the mood for a certain type of music but not any specific artists, songs, or genres in mind? Maybe you just had a long day and are looking for something with mellow and relaxing vibes. Or maybe it's a Friday afternoon, and you're ready for something to pump you up for the weekend. In times like these, traditional search experiences won't work. But today, we're going to build something that will.



In a traditional search experience, you would search for a term and get back a list of songs or artists that contain keywords from that search term somewhere in their name or description. But that's not what we want here. What we actually want is semantic search: a system that understands what you mean, not just what you typed. That's exactly what we're building. This music recommender takes a natural-language description of the music's “vibe” and finds songs that match that feel. And it's all powered by MongoDB Vector Search and Voyage AI.



In this tutorial, you’ll learn how to build a working web application where you can type something like "upbeat road trip with the windows down" and get back a ranked list of songs that fit that feeling. Let's get into it!



. I'll explain all the elements throughout this tutorial, and you can clone the repository and follow along as well.






Prerequisites



Before getting started, you'll need a few things in place:





  • Node.js (v18 or later) installed on your machine

  • A MongoDB Atlas account with a free cluster

  • A Voyage AI API key - you can get one from the with pre-generated "vibe descriptions" and embeddings. You can download the seed files from the index:




    CODE
    npm run seed






    This creates a new database called song_vibe_search and a new collection called songs. The song collection contains 500 documents, each representing a song with a "vibe description" and an embedding vector. It also creates a vector search index on the embedding field by running the following code:




    CODE
    await collection.createSearchIndex({
    name: VECTOR_INDEX_NAME,
    type: "vectorSearch",
    definition: {
    fields: [
    {
    type: "vector",
    path: "embedding",
    numDimensions: 512,
    similarity: "cosine",
    },
    ],
    },
    });






    This is what makes $vectorSearch possible. We need to tell MongoDB the shape of our embedding field so it can build the right index structure.



    The index contains the type of index (in our case, vector), the path to the field, and the following parameters that tell MongoDB how to compare vectors:





    • numDimensions: 512 — This must match the output size of your embedding model. . The $vectorSearch stage takes the following parameters:





      • index — The name of the vector search index we created during seeding


      • path — The field in the document that holds the embedding (in this case, named "embedding").


      • queryVector — The vector we just generated from the user's query.


      • numCandidates — How many candidate documents to consider before ranking. A higher number means the search will be more thorough but slower.


      • limit — How many results to return.



      After the search stage, a $project stage shapes our output, with { $meta: "vectorSearchScore" } pulling in the similarity score for each result — a number between 0 and 1, where higher means more similar.



      And that's all we need for our search functionality! Next, we'll build out a simple server with an endpoint to call this function.






      Building the Server



      Create a new file called server.ts in the src directory. This is where we will create any endpoints we might need. In this tutorial, we will create a single endpoint to search for songs.



      To start, paste the following code into server.ts, which specifies the imports and sets up an Express server:




      CODE
      import "dotenv/config";
      import express from "express";
      import path from "path";
      import { client, searchSongs, VECTOR_INDEX, NUM_RESULTS, NUM_CANDIDATES } from "./search";

      const app = express();
      const PORT = process.env.PORT ?? 3000;

      app.use(express.json());
      app.use(express.static(path.join(__dirname, "../public")));






      Now, we'll create our POST route that calls the searchSongs function we created earlier. Paste the following code into server.ts:




      CODE
      app.post("/api/search", async (req, res) => {
      const { query } = req.body;

      if (!query || typeof query !== "string" || query.trim() === "") {
      res.status(400).json({ error: "A search query is required." });
      return;
      }

      let results, queryVector;
      try {
      ({ results, queryVector } = await searchSongs(query.trim()));
      } catch (err) {
      console.error("Search failed:", err);
      res.status(500).json({ error: "Search failed. Please try again." });
      return;
      }

      res.json({
      results
      });
      });






      In the preceding code, it first checks that the query is valid. Then it sends the query to our searchSongs() function and returns the results. If there is an error, it returns an error message.



      The last thing we need to do is write the code that actually starts the server and handles signals to close the client cleanly on shutdown. Paste the following code into server.ts:




      CODE
      async function startServer() {
      await client.connect();
      console.log("Connected to MongoDB Atlas.");

      app.listen(PORT, () => {
      console.log(`Server running at http://localhost:${PORT}`);
      });

      process.on("SIGINT", async () => {
      await client.close();
      process.exit(0);
      });

      process.on("SIGTERM", async () => {
      await client.close();
      process.exit(0);
      });
      }

      startServer();






      Now that we have our server set up, let's create a simple front end to interact with it.






      Building the Front End



      Since the purpose of this tutorial is the search functionality itself, we'll keep the front end simple. Create a new directory called public in the root of your project (it's important to keep the name as public since our server is already set up to serve files from this directory). Then, create a new file called index.html in that folder.



      Paste the following code into index.html:




      CODE
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Vibe Search</title>
      <link rel="stylesheet" href="style.css" />
      </head>
      <body>
      <div class="container">
      <header>
      <h1>Vibe Search</h1>
      <p>Describe a mood or feeling and we'll find songs that match.</p>
      </header>

      <form id="search-form">
      <input
      id="query-input"
      type="text"
      placeholder="e.g. melancholy rainy afternoon, upbeat road trip..."
      autocomplete="off"
      />
      <button type="submit">Search</button>
      </form>
      <p class="token-note">Note: Each search generates a Voyage AI embedding and consumes tokens.</p>

      <div id="results"></div>
      </div>

      <script src="app.js"></script>
      </body>
      </html>






      This HTML creates a simple page with a header, an input box, and a section for results.



      Now we need to create the style.css and app.js files. Create a new file called style.css in the public directory and paste the following code into it:




      CODE
      *, *::before, *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      }

      body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: #0f0f0f;
      color: #e8e8e8;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 2rem 1rem;
      }

      .container {
      width: 100%;
      max-width: 720px;
      }

      header {
      text-align: center;
      margin-bottom: 2.5rem;
      }

      header h1 {
      font-size: 2.5rem;
      font-weight: 700;
      letter-spacing: -0.5px;
      margin-bottom: 0.5rem;
      }

      header p {
      color: #777;
      font-size: 1rem;
      }

      /* ── Search form ── */

      #search-form {
      display: flex;
      gap: 0.5rem;
      margin-bottom: 2.5rem;
      }

      #query-input {
      flex: 1;
      padding: 0.75rem 1rem;
      font-size: 1rem;
      background: #1a1a1a;
      border: 1px solid #333;
      border-radius: 8px;
      color: #e8e8e8;
      outline: none;
      }

      #query-input:focus {
      border-color: #555;
      }

      #query-input::placeholder {
      color: #555;
      }

      #search-form button {
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      font-weight: 600;
      background: #00ed64;
      color: #000;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      }

      #search-form button:hover {
      background: #00c853;
      }

      /* ── Token note ── */

      .token-note {
      font-size: 1rem;
      color: #989898;
      text-align: center;
      margin-top: 0.6rem;
      margin-bottom: 0;
      }

      /* ── Spinner ── */

      .spinner {
      width: 28px;
      height: 28px;
      border: 3px solid #2a2a2a;
      border-top-color: #00ed64;
      border-radius: 50%;
      margin: 2rem auto;
      animation: spin 0.7s linear infinite;
      }

      @keyframes spin {
      to { transform: rotate(360deg); }
      }

      /* ── Result cards ── */

      #results {
      display: flex;
      flex-direction: column;
      gap: 1rem;
      }

      .card {
      background: #1a1a1a;
      border: 1px solid #2a2a2a;
      border-radius: 12px;
      overflow: hidden;
      }

      .card-color-bar {
      height: 6px;
      }

      .card-body {
      padding: 1.25rem 1.5rem;
      }

      .card-title {
      font-size: 1.1rem;
      font-weight: 600;
      margin-bottom: 0.2rem;
      }

      .card-artist {
      color: #777;
      font-size: 0.9rem;
      margin-bottom: 0.85rem;
      }

      .tags {
      display: flex;
      flex-wrap: wrap;
      gap: 0.35rem;
      margin-bottom: 1rem;
      }

      .tag {
      font-size: 0.75rem;
      padding: 0.2rem 0.65rem;
      border-radius: 999px;
      background: #252525;
      color: #999;
      }

      .card-footer {
      display: flex;
      justify-content: space-between;
      align-items: center;
      }

      .score {
      font-size: 0.8rem;
      color: #555;
      }

      .fma-link {
      font-size: 0.8rem;
      color: #00ed64;
      text-decoration: none;
      }

      .fma-link:hover {
      text-decoration: underline;
      }

      .message {
      text-align: center;
      color: #555;
      padding: 2rem 0;
      font-size: 0.95rem;
      }

      .message.error {
      color: #e05252;
      }






      The CSS added here is outside of the scope of this tutorial, but it just makes the app easier to look at and use.



      The last file we need to create is app.js, which contains the front-end JavaScript code. Create a new file called app.js in the public directory and paste the following code into it:




      CODE
      const DEFAULT_GRADIENT = "linear-gradient(135deg, #434343, #1a1a1a)";

      function renderResults(results) {
      const container = document.getElementById("results");

      if (results.length === 0) {
      container.innerHTML = '<p class="message">No results found. Try a different vibe.</p>';
      return;
      }

      container.innerHTML = results.map((result) => {
      const score = Math.round(result.score * 100);
      const tags = result.genres.map((g) => `<span class="tag">${g}</span>`).join("");

      return `
      <div class="card">
      <div class="card-color-bar" style="background:
      ${DEFAULT_GRADIENT}"></div>
      <div class="card-body">
      <div class="card-title">
      ${result.title}</div>
      <div class="card-artist">
      ${result.artist}</div>
      <div class="tags">
      ${tags}</div>
      <div class="card-footer">
      <span class="score">Match:
      ${score}%</span>
      <a class="fma-link" href="https://www.youtube.com/results?search_query=
      ${encodeURIComponent(result.title + ' ' + result.artist)}" target="_blank" rel="noopener">Find on YouTube →</a>
      </div>
      </div>
      </div>
      `
      ;
      }).join("");
      }

      document.getElementById("search-form").addEventListener("submit", async (e) => {
      e.preventDefault();

      const query = document.getElementById("query-input").value.trim();
      if (!query) return;

      const container = document.getElementById("results");
      container.innerHTML = '<div class="spinner"></div>';

      const response = await fetch("/api/search", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ query }),
      });

      if (!response.ok) {
      container.innerHTML = '<p class="message error">Something went wrong. Please try again.</p>';
      return;
      }

      const { results } = await response.json();
      renderResults(results);
      });






      This JavaScript code adds interactivity to the page. It sends the search query to the server and displays the search results.






      Running the App



      At this point, you should have everything set up. Make sure you have already run the npm run seed\ command from earlier in the tutorial.



      Then, run the following command to start the server:




      CODE
      npm run dev






      Open , you'll notice that there are a few files in the seed directory that we didn't use in this tutorial. The fetchTracks.ts file contains code for downloading metadata about the songs that were downloaded from the Free Music Archive. This file will be specific to the Free Music Archive data, so we won't go into depth on it here.



      The generateVibeDescriptions.ts and generateEmbeddings.ts files contain the code used to generate the "vibe descriptions" and embeddings for the sample data. The generateVibeDescriptions.ts file uses the Gemini API to generate a text description of the "vibe" of each song. The generateEmbeddings.ts file uses the Voyage AI API to generate an embedding for each description.





      Generating Vibe Descriptions



      Before generating embeddings from our data, we need some kind of description to embed. Raw metadata like title, artist, and genre isn't rich enough to embed meaningfully on its own, so we use Google Gemini to generate a 2-3 sentence vibe description for each track. Then, we save these descriptions in a new field in our documents called vibe_description.



      To run the code to generate the descriptions, you'll need to get a Gemini API key. You can do this by going to the or leave a comment letting me know how you’d like to see this expanded on!

      Vollständiger Original-Bericht
      Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
      ↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a Vibe-Based Music Recommender with MongoDB and Voyage AI

Thematisch verwandte Begriffe: Building, VibeBased, Music, Recommender · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...