Lightweight Search Engine

API Reference

API Reference

Complete reference for the Hugo Lightweight Search PHP API.

Endpoint

GET /api/search.php

Base URL: https://yourdomain.com/api/search.php

Authentication

The API is publicly accessible and does not require authentication. CORS is enabled to allow cross-origin requests.

Query Parameters

Search Action (Default)

ParameterTypeRequiredDefaultDescription
qstringYes-Search query (minimum 2 characters)
pageintegerNo1Page number for pagination (1-indexed)
sectionstringNo-Filter results by section (e.g., “blog”, “docs”)
sortstringNorelevanceSort order: relevance, date_desc, or date_asc
limitintegerNo20Results per page (maximum 100)

Sections Action

ParameterTypeRequiredDescription
actionstringYesSet to sections to retrieve available sections

Advanced Query Syntax

The search API supports advanced query operators for precise searching:

Use double quotes to search for exact phrases:

"machine learning"

Field-Specific Searches

Search within specific fields using field:term syntax:

Examples:

title:tutorial
tags:javascript
content:"error handling"

Date Filters

Filter results by date range:

Examples:

after:2024-01-01
before:2024-12-31
after:2024-01-01 before:2024-12-31

Boolean Operators

Examples:

javascript OR python
react AND hooks

Wildcard and Fuzzy Matching

Examples:

java*    (matches java, javascript, javadoc)
react    (automatically searches for react*)

Combining Query Operators

You can combine multiple operators in a single query:

title:"getting started" after:2024-01-01 tags:tutorial
"docker compose" OR kubernetes content:deployment

Response Format

Successful Search Response

{
  "results": [
    {
      "id": "1",
      "title": "Getting Started with Hugo",
      "title_highlighted": "Getting Started with <mark>Hugo</mark>",
      "url": "/blog/getting-started-with-hugo/",
      "summary": "Learn how to build static sites with Hugo",
      "summary_highlighted": "Learn how to build static sites with <mark>Hugo</mark>",
      "content_snippet": "...quick start guide for <mark>Hugo</mark> static site...",
      "date": "2024-03-15",
      "section": "blog",
      "tags": ["hugo", "tutorial", "static-site"],
      "categories": ["web-development"],
      "relevance": -2.45,
      "relevance_score": 2.45
    }
  ],
  "total": 42,
  "page": 1,
  "per_page": 20,
  "total_pages": 3,
  "query": "hugo tutorial",
  "parsed_query": {
    "terms": ["hugo", "tutorial"],
    "phrases": [],
    "field_searches": [],
    "after": null,
    "before": null,
    "operators": []
  },
  "fts_query": "hugo* AND tutorial*"
}

Sections Response

{
  "sections": [
    "blog",
    "docs",
    "tutorials"
  ]
}

Error Response

{
  "error": "Search error: Database connection failed"
}

HTTP status code will be 500 for errors.

Result Object Fields

Each search result contains:

FieldTypeDescription
idstringUnique identifier for the content
titlestringOriginal title
title_highlightedstringTitle with search terms wrapped in <mark> tags
urlstringRelative URL to the content
summarystringOriginal summary/description
summary_highlightedstringSummary with search terms highlighted
content_snippetstringExcerpt from content with search terms highlighted
datestringPublication date (YYYY-MM-DD format)
sectionstringContent section (e.g., “blog”, “docs”)
tagsarrayArray of tag strings
categoriesarrayArray of category strings
relevancenumberRaw BM25 relevance score (negative value)
relevance_scorenumberNormalized relevance score (positive, rounded to 2 decimals)

Sorting Options

relevance (default)

Results are sorted by BM25 relevance score with intelligent boosting:

  1. Documents with the search term in the title are ranked higher
  2. More relevant matches (based on BM25 algorithm) appear first
  3. Recent content gets slight preference when relevance is equal

date_desc

Results sorted by publication date, newest first. Relevance is used as a secondary sort key.

date_asc

Results sorted by publication date, oldest first. Relevance is used as a secondary sort key.

Pagination

The API uses offset-based pagination:

To fetch the next page:

?q=search&page=2

Example Requests

cURL:

curl "https://yourdomain.com/api/search.php?q=hugo"

JavaScript (Fetch):

fetch('https://yourdomain.com/api/search.php?q=hugo')
  .then(response => response.json())
  .then(data => {
    console.log(`Found ${data.total} results`);
    data.results.forEach(result => {
      console.log(result.title, result.url);
    });
  });

JavaScript (Async/Await):

async function search(query) {
  const response = await fetch(
    `https://yourdomain.com/api/search.php?q=${encodeURIComponent(query)}`
  );
  const data = await response.json();
  return data;
}

const results = await search('hugo tutorial');

Search with Pagination

cURL:

curl "https://yourdomain.com/api/search.php?q=javascript&page=2&limit=10"

JavaScript:

const params = new URLSearchParams({
  q: 'javascript',
  page: 2,
  limit: 10
});

const response = await fetch(`/api/search.php?${params}`);
const data = await response.json();

Filter by Section

cURL:

curl "https://yourdomain.com/api/search.php?q=tutorial&section=blog"

JavaScript:

const response = await fetch('/api/search.php?q=tutorial&section=blog');
const data = await response.json();

Sort by Date

cURL:

curl "https://yourdomain.com/api/search.php?q=news&sort=date_desc"

JavaScript:

const response = await fetch('/api/search.php?q=news&sort=date_desc');
const data = await response.json();

Advanced Query with Multiple Operators

cURL:

curl "https://yourdomain.com/api/search.php?q=title:docker%20after:2024-01-01%20tags:tutorial"

JavaScript:

const query = 'title:docker after:2024-01-01 tags:tutorial';
const response = await fetch(
  `/api/search.php?q=${encodeURIComponent(query)}`
);
const data = await response.json();

Exact Phrase Search

cURL:

curl "https://yourdomain.com/api/search.php?q=\"getting%20started\""

JavaScript:

const response = await fetch(
  '/api/search.php?q=' + encodeURIComponent('"getting started"')
);
const data = await response.json();

Get Available Sections

cURL:

curl "https://yourdomain.com/api/search.php?action=sections"

JavaScript:

const response = await fetch('/api/search.php?action=sections');
const { sections } = await response.json();
console.log('Available sections:', sections);

Complete Search UI Example

Here’s a complete example of building a search interface:

class SearchUI {
  constructor(apiUrl) {
    this.apiUrl = apiUrl;
    this.currentPage = 1;
    this.currentQuery = '';
  }

  async search(query, options = {}) {
    const params = new URLSearchParams({
      q: query,
      page: options.page || this.currentPage,
      limit: options.limit || 20,
      sort: options.sort || 'relevance'
    });

    if (options.section) {
      params.append('section', options.section);
    }

    try {
      const response = await fetch(`${this.apiUrl}?${params}`);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();

      if (data.error) {
        throw new Error(data.error);
      }

      return data;
    } catch (error) {
      console.error('Search failed:', error);
      throw error;
    }
  }

  async getSections() {
    const response = await fetch(`${this.apiUrl}?action=sections`);
    const data = await response.json();
    return data.sections;
  }

  renderResults(data) {
    const resultsContainer = document.getElementById('search-results');
    const html = data.results.map(result => `
      <article class="search-result">
        <h3>
          <a href="${result.url}">${result.title_highlighted}</a>
        </h3>
        <div class="metadata">
          <span class="date">${result.date}</span>
          <span class="section">${result.section}</span>
          <span class="score">Score: ${result.relevance_score}</span>
        </div>
        <p class="summary">${result.summary_highlighted}</p>
        ${result.content_snippet ?
          `<p class="snippet">${result.content_snippet}</p>` : ''
        }
        ${result.tags.length ?
          `<div class="tags">${result.tags.map(tag =>
            `<span class="tag">${tag}</span>`
          ).join('')}</div>` : ''
        }
      </article>
    `).join('');

    resultsContainer.innerHTML = html;

    // Render pagination
    this.renderPagination(data);
  }

  renderPagination(data) {
    const pagination = document.getElementById('pagination');
    const pages = [];

    for (let i = 1; i <= data.total_pages; i++) {
      const isActive = i === data.page ? 'active' : '';
      pages.push(`
        <button class="${isActive}" data-page="${i}">
          ${i}
        </button>
      `);
    }

    pagination.innerHTML = `
      <div class="pagination-info">
        Showing ${(data.page - 1) * data.per_page + 1}-${Math.min(data.page * data.per_page, data.total)}
        of ${data.total} results
      </div>
      <div class="pagination-buttons">
        ${pages.join('')}
      </div>
    `;

    // Add click handlers
    pagination.querySelectorAll('button').forEach(btn => {
      btn.addEventListener('click', () => {
        this.currentPage = parseInt(btn.dataset.page);
        this.search(this.currentQuery, { page: this.currentPage })
          .then(data => this.renderResults(data));
      });
    });
  }
}

// Usage
const searchUI = new SearchUI('/api/search.php');

document.getElementById('search-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const query = document.getElementById('search-input').value;
  const section = document.getElementById('section-filter').value;
  const sort = document.getElementById('sort-order').value;

  searchUI.currentQuery = query;
  searchUI.currentPage = 1;

  const results = await searchUI.search(query, { section, sort });
  searchUI.renderResults(results);
});

Rate Limiting

The API does not currently implement rate limiting. For production use, consider implementing rate limiting at the web server level (e.g., using nginx limit_req module).

Performance Tips

  1. Use specific field searches when possible (e.g., title:term) to narrow results
  2. Limit results per page to reduce response size and improve load times
  3. Cache section listings since they change infrequently
  4. Implement client-side debouncing for search-as-you-type features
  5. Use pagination rather than fetching all results at once

Database Structure

The API uses SQLite with FTS5 (Full-Text Search 5) for high-performance searches. The database contains:

Searches use BM25 ranking algorithm for relevance scoring.

Error Handling

The API returns JSON error responses with appropriate HTTP status codes:

Always check for the error field in the response:

const response = await fetch('/api/search.php?q=test');
const data = await response.json();

if (data.error) {
  console.error('Search error:', data.error);
  // Handle error appropriately
} else {
  // Process results
}