A free and simple Algolia and TypeSense Alternative with Para backend

HTML

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/daisyui/5.1.25/daisyui.css">

<button class="btn btn-ghost btn-sm" onclick="site_search_modal.showModal()" aria-label="Open search" title="Search '/' or Ctrl+K">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
  <circle cx="11" cy="11" r="7"></circle>
  <path d="M20 20l-3.5-3.5"></path>
</svg>
SEARCH
</button>

<dialog id="site_search_modal" class="modal">
  <div class="top-24 absolute xs:w-full sm:w-10/12 md:w-6/12 max-w-3xl modal-box">
    <div class="mt-1">
      <div class="flex items-center gap-2 w-full input input-bordered">
        <svg class="opacity-70 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <circle cx="11" cy="11" r="7"></circle>
          <path d="M20 20l-3.5-3.5"></path>
        </svg>
        <input id="site-search-input" type="search" list="options" class="grow" placeholder="Search…"
          role="combobox" aria-controls="searchbox" aria-expanded="true" aria-autocomplete="list"/>
      </div>
    </div>
    <ul id="site-search-results" class="space-y-2 mt-3 text-sm"></ul>
    <div class="flex pt-4 text-xs">
      <span class="grow">
        <kbd class="kbd kbd-sm">▲</kbd> <kbd class="kbd kbd-sm">▼</kbd>
        <kbd class="kbd kbd-sm">Enter</kbd> to select, 
        <kbd class="kbd kbd-sm">Esc</kbd> to close
      </span>
      <strong class="justify-end text-right">
        <a href="https://paraio.org">Search powered by Para</a>
      </strong>
    </div>
  </div>
  <form method="dialog" class="modal-backdrop">
    <button>close</button>
  </form>
</dialog>

Tailwind CSS

/* There's nothing here - CSS is embedded in the HTML. */

JavaScript

const appID = "app:myapp";
const isLocal = false;
const paraEndpoint = isLocal ? "http://localhost:8000" : "https://paraio.com";
const searchToggle = document.getElementById("site_search_modal");
const searchInput = document.getElementById("site-search-input");
const resultsList = document.getElementById("site-search-results");
const noResults = "<li>No Results</li>";
let searchAbortController;
let searchDebounce;
let activeIndex = -1;

const openSearch = () => {
  if (searchToggle) {
    searchToggle.showModal();
  }
};

const renderResults = (items) => {
  if (!resultsList) return;
  resultsList.innerHTML = items.length === 0 ? noResults : "";
  activeIndex = -1;
  items.forEach((item) => {
    const li = document.createElement("li");
    const link = document.createElement("a");
    link.href = item.url;
    link.textContent = item.name;
    link.className = "block rounded-[3px] px-3 py-3";
    li.className = "border border-base-300 hover:border-sky-700 rounded-[3px]";
    li.appendChild(link);
    resultsList.appendChild(li);
  });
};

const updateActiveResult = (index) => {
  if (!resultsList) return;
  const items = Array.from(resultsList.querySelectorAll("li"));
  items.forEach((item, i) => {
    if (i === index) {
      item.classList.add("border-sky-700", "bg-base-200");
    } else {
      item.classList.remove("border-sky-700", "bg-base-200");
    }
  });
};

const search = async (query) => {
  if (!resultsList) return;
  const trimmed = query.trim();
  if (!trimmed) {
    resultsList.innerHTML = "";
    return;
  }
  if (searchAbortController) {
    searchAbortController.abort();
  }
  searchAbortController = new AbortController();
  try {
    const response = await fetch(
      `${paraEndpoint}/v1/blogposts?limit=6&q=${encodeURIComponent(trimmed || "*")}`,
      {
        signal: searchAbortController.signal,
        headers: {
          Authorization: "Anonymous " + appID,
        },
   ...