Skip to content

Add AI Search to Your App

This guide explains how to add AI-powered semantic search to your application.

Overview

Adding AI search to your application involves two main steps:

  1. Adding documents to your semantic index
  2. Implementing search in your application

Adding Documents

First, you'll need to add documents to your Gainly semantic index. You can:

Once you have documents in your index, you can implement search using the Search Documents endpoint:

import requests

def search_documents(query: str) -> dict:
    """
    Search documents using Gainly's AI semantic search.

    Args:
        query: The search query
    """
    BASE_URL = "https://api.gainly.ai"
    VERSION = "v20241104"
    headers = {
        "Content-Type": "application/json",
        "X-API-Key": "YOUR_API_KEY_HERE"  # Replace with your actual API key
    }

    response = requests.post(
        f"{BASE_URL}/{VERSION}/search",
        headers=headers,
        json={
            "query": query,
            "search_type": "hybrid"
        }
    )
    return response.json()

# Example usage
results = search_documents("what are the benefits of alpaca fleece?")

The search endpoint returns relevant documents ranked by relevance. You can then use the results to display search results in your application.

Example API Response

{
    "object": "search_result",
    "url": "/v20241104/search",
    "query": "what are the benefits of alpaca fleece?",
    "search_type": "hybrid",
    "hybrid_search_ai_ratio": 0.6,
    "data": [
        {
            "id": "4nD1gZIB7caKVIeL2MgL",
            "title": "**Alpaca** **Fleece** and Its Economic Impact",
            "excerpt": "...**Alpaca** **Fleece** and Its Economic Impact... **Alpaca** **fleece** is one of the most valued natural fibers in the world, known for its softness, warmth,... Unlike sheep's wool, **alpaca** **fleece** does not contain lanolin, making it hypoallergenic and more suitable... There are two types of **alpaca** **fleece**: Huacaya and Suri... Huacaya **fleece** is crimped, giving it a fluffy appearance, while Suri **fleece** is silky and forms long,...",
            "confidence_level": "very_high",
            "metadata": null,
            "source_uri": "/doc/alpaca-fleece-economic-20241012a",
            "tenant_id": "tenant226",
            "language": "en",
            "sort_value": null,
            "created_at": "2024-10-12T18:20:25.358391Z",
            "updated_at": "2024-10-12T18:20:25.358405Z"
        },
        {
            "id": "B3AegpIB7caKVIeL78nJ",
            "title": "**Alpaca** Fiber Processing and Textile Production",
            "excerpt": "...**Alpaca** yarn is prized for its softness, warmth, and durability, making it a favorite among knitters,... The natural colors of **alpaca** **fleece**, ranging from white to black and various shades of brown and gray... but also support local economies in **alpaca**-producing regions... The craftsmanship involved in transforming raw **fleece** into finished goods is a testament to the enduring... value and appeal of **alpaca** fiber in the global market...",
            "confidence_level": "high",
            "metadata": null,
            "source_uri": "/doc/alpaca-fiber-textile-20241012",
            "tenant_id": "tenant226",
            "language": "en",
            "sort_value": null,
            "created_at": "2024-10-12T19:05:18.420115Z",
            "updated_at": "2024-10-12T19:05:18.420159Z"
        },
        {
            "id": "DHAggpIB7caKVIeLN8lu",
            "title": "**Alpaca** Fiber and the Global Fashion Industry",
            "excerpt": "...**Alpaca** Fiber and the Global Fashion Industry... **Alpaca** fiber has gained significant recognition in the global fashion industry due to its luxurious qualities... Known for its softness, warmth, and durability, **alpaca** fiber is often compared to cashmere and is highly... Unlike sheep wool, **alpaca** fiber lacks lanolin, making it hypoallergenic and suitable for people with... natural luster, giving it a silky appearance that enhances the visual appeal of garments made from **alpaca**...",
            "confidence_level": "medium",
            "metadata": null,
            "source_uri": "/doc/alpaca-fiber-global-fashion-20241012",
            "tenant_id": "tenant226",
            "language": "en",
            "sort_value": null,
            "created_at": "2024-10-12T19:06:42.297067Z",
            "updated_at": "2024-10-12T19:06:42.297080Z"
        }
    ],
    "tenant_id": null,
    "filter": null,
    "sort": [
        {
            "_score": {
                "order": "desc"
            }
        }
    ],
    "facet": null,
    "facet_result": {},
    "language": "en",
    "multilingual_search": false,
    "fuzzy_search": null,
    "has_more": false,
    "next_page": null,
    "total_number_of_results": 3,
    "page": 1,
    "limit": 10,
    "highlights": true,
    "excerpts": true,
    "ai_search_cutoff_score": 35,
    "token_usage": {
        "semantic_tokens": 5,
        "llm_tokens": {
            "llm_output_tokens": 0,
            "llm_input_tokens": 0,
            "model": null
        }
    },
    "livemode": false
}

Displaying Results

Here's how to create a simple search interface using Node.js/Express:

// server.js
const express = require('express');
const app = express();

app.use(express.json());
app.use(express.static('public'));  // Serve static files from 'public' directory

// HTML template
const HTML_TEMPLATE = `
<!DOCTYPE html>
<html>
<head>
    <style>
        .search-container {
            max-width: 800px;
            margin: 2rem auto;
        }
        .search-form {
            margin-bottom: 2rem;
            display: flex;
            gap: 1rem;
        }
        .search-input {
            flex: 1;
            padding: 0.5rem;
            font-size: 1rem;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .search-button {
            padding: 0.5rem 1rem;
            background: #1a73e8;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .search-button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .search-results {
            font-family: system-ui, -apple-system, sans-serif;
        }
        .result-item {
            margin-bottom: 2rem;
            padding: 1rem;
            border-radius: 8px;
            background: #f8f9fa;
        }
        .result-title {
            color: #1a0dab;
            font-size: 1.2rem;
            margin-bottom: 0.5rem;
            text-decoration: none;
        }
        .result-title:hover {
            text-decoration: underline;
        }
        .result-excerpt {
            color: #4d5156;
            font-size: 0.95rem;
            line-height: 1.5;
        }
        .load-more {
            display: block;
            width: 100%;
            padding: 0.75rem;
            margin-top: 2rem;
            background: #f8f9fa;
            border: 1px solid #ddd;
            border-radius: 4px;
            cursor: pointer;
        }
        .load-more:hover {
            background: #f1f3f4;
        }
        .load-more:disabled {
            background: #eee;
            cursor: not-allowed;
        }
        .results-summary {
            color: #70757a;
            margin-bottom: 1rem;
            font-size: 0.9rem;
        }
    </style>
</head>
<body>
    <div class="search-container">
        <form class="search-form" onsubmit="handleSearch(event)">
            <input type="text" class="search-input" id="search-input" placeholder="Search...">
            <button type="submit" class="search-button">Search</button>
        </form>

        <div id="search-results" class="search-results">
            <div id="results-summary" class="results-summary"></div>
            <div id="results-list"></div>
            <button id="load-more" class="load-more" style="display: none" onclick="loadMore()">
                Load More Results
            </button>
        </div>
    </div>

    <script>
        let currentQuery = '';
        let nextPage = null;
        let searching = false;

        function displayResults(data, append = false) {
            const resultsDiv = document.getElementById('results-list');
            const summaryDiv = document.getElementById('results-summary');
            const loadMoreBtn = document.getElementById('load-more');

            // Update results summary
            summaryDiv.textContent = `Showing ${resultsDiv.children.length + data.data.length} of ${data.total_number_of_results} results`;

            // Add new results
            const newResults = data.data.map(result => `
                <div class="result-item">
                    <a href="${result.source_uri}" class="result-title">
                        ${result.title}
                    </a>
                    <div class="result-excerpt">
                        ${result.excerpt}
                    </div>
                </div>
            `).join('');

            if (append) {
                resultsDiv.innerHTML += newResults;
            } else {
                resultsDiv.innerHTML = newResults;
            }

            // Update load more button
            nextPage = data.next_page;
            loadMoreBtn.style.display = data.has_more ? 'block' : 'none';
            loadMoreBtn.disabled = searching;
        }

        async function performSearch(query, page = null) {
            const searchButton = document.querySelector('.search-button');
            const loadMoreBtn = document.getElementById('load-more');

            searching = true;
            searchButton.disabled = true;
            loadMoreBtn.disabled = true;

            try {
                const response = await fetch('/api/search', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ 
                        query,
                        page,
                        limit: 10  // Number of results per page
                    })
                });

                const data = await response.json();
                return data;
            } catch (error) {
                console.error('Search error:', error);
                document.getElementById('results-list').innerHTML = 
                    '<div class="error">Error performing search</div>';
                throw error;
            } finally {
                searching = false;
                searchButton.disabled = false;
                loadMoreBtn.disabled = false;
            }
        }

        async function handleSearch(e) {
            e.preventDefault();
            const query = document.getElementById('search-input').value;
            currentQuery = query;

            try {
                const data = await performSearch(query);
                displayResults(data);
            } catch (error) {
                // Error already handled in performSearch
            }
        }

        async function loadMore() {
            if (!nextPage || searching) return;

            try {
                const data = await performSearch(currentQuery, nextPage);
                displayResults(data, true);
            } catch (error) {
                // Error already handled in performSearch
            }
        }
    </script>
</body>
</html>`;

// Search endpoint
app.post('/api/search', async (req, res) => {
    const GAINLY_API_KEY = process.env.GAINLY_API_KEY;
    const BASE_URL = 'https://api.gainly.ai';
    const VERSION = 'v20241104';

    try {
        const response = await fetch(`${BASE_URL}/${VERSION}/search`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-API-Key': GAINLY_API_KEY
            },
            body: JSON.stringify({
                query: req.body.query,
                search_type: 'hybrid',
                page: req.body.page || null,
                limit: req.body.limit || 10
            })
        });

        const data = await response.json();
        res.json(data);
    } catch (error) {
        console.error('Error calling Gainly API:', error);
        res.status(500).json({ error: 'Failed to perform search' });
    }
});

// Serve the HTML
app.get('/', (req, res) => {
    res.send(HTML_TEMPLATE);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

This example includes:

  • A Node.js/Express server that securely proxies requests to Gainly API
  • A search form with modern styling
  • Real-time search results display
  • Proper error handling
  • Security best practices (API key stored in environment variables)

To run this example:

Prerequisites

  • Sign up for a free Gainly account.
  • Add documents to your semantic index as explained at the start of this guide.
  1. Install the required dependencies:

    npm init -y
    npm install express node-fetch
    

  2. Save the code to server.js

  3. Set your Gainly API key as an environment variable:

    export GAINLY_API_KEY=your_api_key_here
    

  4. Run the application:

    node server.js
    

  5. Open your browser to http://localhost:3000

Next Steps