Searching Your Site Without a Database

Learn how to work with fuzzy search, scoring and multiple fields and allow your Nuxt app to search static data.


This content originally appeared on Telerik Blogs and was authored by Jonathan Gamble

Learn how to work with fuzzy search, scoring and multiple fields and allow your Nuxt app to search static data.

Do you ever see documentation websites that have a search feature and it’s super fast? Well, the novices decide to use a real database like Algolia and potentially pay for it. The pros use pure JavaScript.

TL;DR

Learn to create a Nuxt application that can search static data. Set the data as an object, then filter through the results in real time to get the desired page. This demo adds fuzzy search, uses scoring and handles multiple fields.

Search box where user has entered mary mc, and suggestions come up for Marty McFly and Enchantment Under the Sea Dance

Nuxt

This demo uses Nuxt and assumes you have a basic understanding of composables, pages and layouts.

Layout

Create the default layout.

// layouts/default.vue

<template>
  <main class="min-h-screen bg-gray-100 text-gray-800">
    <div class="flex flex-col items-center py-8">
      <img src="https://www.telerik.com/bttf.webp" alt="Back to the Future man!" />
    </div>
    <div class="max-w-4xl mx-auto px-6">
      <slot />
    </div>
    <nav class="bg-white border-t border-gray-200 mt-12 py-8">
      <div class="max-w-4xl mx-auto px-6">
        <h2 class="text-xl font-bold text-blue-700 mb-4">
          Back to the Future Archive
        </h2>
        <ul class="grid grid-cols-1 sm:grid-cols-2 gap-3">
          <li>
            <NuxtLink to="/" class="hover:underline text-blue-600">
              Home
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/vehicles/delorean"
              class="hover:underline text-blue-600"
            >
              The DeLorean Time Machine
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/characters/marty"
              class="hover:underline text-blue-600"
            >
              Marty McFly
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/characters/doc-brown"
              class="hover:underline text-blue-600"
            >
              Doc Emmett Brown
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/timeline/hill-valley"
              class="hover:underline text-blue-600"
            >
              Hill Valley Timeline
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/tech/hoverboard"
              class="hover:underline text-blue-600"
            >
              Hoverboard Technology
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/characters/biff"
              class="hover:underline text-blue-600"
            >
              Biff Tannen's Antagonism
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/tech/flux-capacitor"
              class="hover:underline text-blue-600"
            >
              Flux Capacitor
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/timeline/1985-vs-2015"
              class="hover:underline text-blue-600"
            >
              1985 vs. 2015
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/events/enchantment-dance"
              class="hover:underline text-blue-600"
            >
              Enchantment Under the Sea Dance
            </NuxtLink>
          </li>
          <li>
            <NuxtLink
              to="/shop/merchandise"
              class="hover:underline text-blue-600"
            >
              Back to the Future Merchandise
            </NuxtLink>
          </li>
        </ul>
      </div>
    </nav>
  </main>
</template>

This is just some links to Back to the Future pages.

Create Catch-All Route

I didn’t feel like manually creating a bunch of pages, so I just generated them. In a real world app, you will probably have generated static files created in markdown. If you generate pages from a database, you would probably use the database itself to search.

// pages/[...slug].vue

<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();

const fullPath = "/" + (route.params.slug as string[]).join("/");
const page = items.find((item) => item.url === fullPath);
</script>

<template>
  <div v-if="page">
    <h1>{{ page.title }}</h1>
    <p>{{ page.description }}</p>
  </div>
  <div v-else>
    <h1>404 - Page Not Found</h1>
    <p>The requested page does not exist.</p>
  </div>
</template>

Again, this is for demo purposes only.

Searching: The Actual Data

We want to store our data in a large object.

// composables/useSearch.ts

type BTTFItem = {
    title: string;
    description: string;
    url: string;
};

export const items: BTTFItem[] = [
    {
        title: "The DeLorean Time Machine",
        description: "Explore the iconic DeLorean, the time-traveling car built by Doc Brown.",
        url: "/vehicles/delorean"
    },
    {
        title: "Marty McFly",
        description: "Learn all about the skateboarding teenager who travels through time.",
        url: "/characters/marty"
    },
    {
        title: "Doc Emmett Brown",
        description: "Meet the eccentric inventor behind the time machine.",
        url: "/characters/doc-brown"
    },
    {
        title: "Hill Valley Timeline",
        description: "A deep dive into the changing history of Hill Valley across the trilogy.",
        url: "/timeline/hill-valley"
    },
    {
        title: "Hoverboard Technology",
        description: "Discover the future of personal transportation with hoverboards.",
        url: "/tech/hoverboard"
    },
    {
        title: "Biff Tannen's Antagonism",
        description: "Explore the many timelines where Biff makes life difficult for Marty.",
        url: "/characters/biff"
    },
    {
        title: "Flux Capacitor",
        description: "The core component that makes time travel possible.",
        url: "/tech/flux-capacitor"
    },
    {
        title: "1985 vs. 2015",
        description: "Compare the original 1985 to the future version envisioned in Part II.",
        url: "/timeline/1985-vs-2015"
    },
    {
        title: "Enchantment Under the Sea Dance",
        description: "The pivotal high school dance that almost erased Marty from existence.",
        url: "/events/enchantment-dance"
    },
    {
        title: "Back to the Future Merchandise",
        description: "Browse collectibles, clothes, and posters from the BTTF universe.",
        url: "/shop/merchandise"
    }
];

How Search Works

There is no secret to searching in Vanilla JS. We use a filter.

items.filter(item => item.title.includes(q));

Now, we must check for lowercase, check both title and description, deal with white space before and after, and handle signals.

useSearch

For our first simplest version, we create a composable.

export function useSearch() {

    const query = ref('');
    const results = ref<BTTFItem[]>([]);

    function search() {
        const q = query.value.trim().toLowerCase();
        results.value = q === ''
            ? []
            : items.filter(item =>
                item.title.toLowerCase().includes(q) ||
                item.description.toLowerCase().includes(q)
            );
    }
    return {
        query,
        results,
        search
    };
}

This composable searches through our items array and filters out the results.

Notice the items array is declared outside the hook itself, as we only need to declare the static data once.

Usage

We need to import the the composable and use it in our search component. Make sure to use proper separation of concerns and the single responsibility principle.

Keep the functionality in the composable.

<script setup lang="ts">
const { query, results, search } = useSearch();
</script>

<template>

  <div class="p-6 max-w-xl mx-auto relative">
    <input
      type="text"
      v-model="query"
      @input="search"
      placeholder="Search Back to the Future..."
      class="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
    />

    <transition
      enter-active-class="transition duration-200 ease-out"
      enter-from-class="opacity-0"
      enter-to-class="opacity-100"
      leave-active-class="transition duration-150 ease-in"
      leave-from-class="opacity-100"
      leave-to-class="opacity-0"
    >
      <ul
        v-if="query && results.length"
        class="absolute left-0 right-0 mt-2 z-50 bg-white border border-gray-200 rounded-lg shadow-lg max-h-80 overflow-y-auto"
      >
        <li
          v-for="(item, index) in results"
          :key="index"
          class="hover:bg-gray-50 border-b border-gray-100 last:border-0"
        >
          <NuxtLink
            :to="item.url"
            class="block px-4 py-3 text-blue-600 font-medium"
          >
            {{ item.title }}
          </NuxtLink>
        </li>
      </ul>
    </transition>

    <transition
      enter-active-class="transition duration-200 ease-out"
      enter-from-class="opacity-0"
      enter-to-class="opacity-100"
      leave-active-class="transition duration-150 ease-in"
      leave-from-class="opacity-100"
      leave-to-class="opacity-0"
    >
      <p
        v-if="query && !results.length"
        class="absolute left-0 right-0 mt-2 z-50 bg-white border border-gray-200 rounded-lg shadow-lg px-4 py-3 text-center text-gray-500 italic"
      >
        No results found.
      </p>
    </transition>
  </div>
</template>

Vue has some beautiful transition effects that work great with Tailwind.

For basic filtering examples:

I know, I know, you want more! You want to implement a basic fuzzy search too!

Version 1

function fuzzyMatch(source: string, target: string): boolean {
  source = source.toLowerCase();
  target = target.toLowerCase();

  let sIndex = 0;
  for (let i = 0; i < target.length; i++) {
    if (source[sIndex] === target[i]) {
      sIndex++;
    }
    if (sIndex === source.length) {
      return true;
    }
  }
  return false;
}

export function useSearch() {

  const query = ref('');
  const results = ref<BTTFItem[]>([]);

  function search() {
    const q = query.value.trim().toLowerCase();
    if (q === '') {
      results.value = [];
      return;
    }

    results.value = items.filter(item =>
      fuzzyMatch(q, item.title) || fuzzyMatch(q, item.description)
    );
  }

  return {
    query,
    results,
    search
  };
}

The fuzzyMatch algorithm checks if all characters match in order or not.

Example 1

fuzzyMatch("doc", "Doc Emmett Brown") // true
  • Matches: d → found at index 0, o → index 1, c → index 2 (all in order ✅)

Example 2

fuzzyMatch("mcf", "Marty McFly") // true
  • Matches: m (Marty), c (McFly), f (McFly)—all in order ✅

Example 3

fuzzyMatch("dcm", "Doc Emmett Brown") // false
  • d matches, c matches—but there’s no m after c in the string ❌

If you want to ignore spaces, you can add:

source = source.replace(/\s+/g, '').toLowerCase();
target = target.replace(/\s+/g, '').toLowerCase();

Scoring

Now, let’s score the results so we can sort the order.

function fuzzyScore(query: string, text: string) {

    query = query.replace(/\s+/g, '').toLowerCase();
    text = text.replace(/\s+/g, '').toLowerCase();

    let score = 0;
    let lastIndex = -1;

    for (const char of query) {
        const index = text.indexOf(char, lastIndex + 1);
        if (index === -1) return 0;
        score += 1 / (index - lastIndex);
        lastIndex = index;
    }

    return score;
}

export function useSearch() {

    const query = ref('');
    const results = ref<BTTFItem[]>([]);

    function search() {
        const q = query.value.trim().toLowerCase();
        if (!q) {
            results.value = [];
            return;
        }

        results.value = items
            .map(item => {
                const scoreTitle = fuzzyScore(q, item.title);
                const scoreDesc = fuzzyScore(q, item.description);
                const totalScore = scoreTitle * 2 + scoreDesc;
                return { item, score: totalScore };
            })
            .filter(entry => entry.score > 0)
            .sort((a, b) => b.score - a.score)
            .map(entry => entry.item);
    }

    return {
        query,
        results,
        search
    };
}

Instead of returning a boolean, we return a score and use .sort() to sort by that score. This includes the description AND the title.

Beautiful!

This is just the beginning of what you can do, but now you have the fundamentals!

The future isn’t written yet.

Repo: GitHub
Demo: Vercel


This content originally appeared on Telerik Blogs and was authored by Jonathan Gamble


Print Share Comment Cite Upload Translate Updates
APA

Jonathan Gamble | Sciencx (2025-08-22T14:15:00+00:00) Searching Your Site Without a Database. Retrieved from https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/

MLA
" » Searching Your Site Without a Database." Jonathan Gamble | Sciencx - Friday August 22, 2025, https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/
HARVARD
Jonathan Gamble | Sciencx Friday August 22, 2025 » Searching Your Site Without a Database., viewed ,<https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/>
VANCOUVER
Jonathan Gamble | Sciencx - » Searching Your Site Without a Database. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/
CHICAGO
" » Searching Your Site Without a Database." Jonathan Gamble | Sciencx - Accessed . https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/
IEEE
" » Searching Your Site Without a Database." Jonathan Gamble | Sciencx [Online]. Available: https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/. [Accessed: ]
rf:citation
» Searching Your Site Without a Database | Jonathan Gamble | Sciencx | https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.