Skip to main content
This tutorial shows you how to build custom search functionality using the Gloo AI Search API. You’ll learn to authenticate, perform semantic search, work with rich results, and optionally combine search with Completions V2 for Retrieval Augmented Generation (RAG). The Search API gives you full control over how search works in your application — from the query to the UI. Whether you’re building a knowledge base, a chatbot, or a content discovery experience, this tutorial covers the backend patterns you need.
The Discovery Widget in Gloo AI Studio provides a quick, embeddable search experience. This tutorial shows you how to build equivalent functionality using the Search API directly, giving you full control over branding, UI, and integration patterns.

Prerequisites

Before starting, ensure you have:

Working Code Sample

Follow along with complete working examples in all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java). Includes a proxy server and browser-based frontend for each language.Setup and testing instructions are provided later.
The code snippets in this tutorial are simplified and self-contained — designed for readability and easy copy-paste. The cookbook examples use a modular architecture plus production niceties. Both implement the same APIs and patterns.

Understanding the Search API

The Search API provides AI-powered semantic search across your ingested content. Unlike keyword search, semantic search understands the meaning behind queries — so a search for “secrets to a happy marriage” will find content about “rules for keeping a marriage healthy” even without exact word matches. Endpoint: POST /ai/data/v1/search

Key Features

  • Semantic Search: Near-text search that understands meaning, not just keywords
  • Rich Metadata: AI-generated summaries, biblical analysis, content classifications
  • Snippet Extraction: Pre-chunked content ready for display or RAG
  • Relevance Scoring: Distance, certainty, and score metrics for ranking

Required Parameters

ParameterDescription
queryThe search query string
collectionAlways "GlooProd"
tenantYour publisher (tenant) name
limitNumber of results to return (10-100 recommended)

Optional Parameters

ParameterTypeDescription
certaintyfloat (0-1)Minimum relevance threshold. The Search Playground defaults to 0.5. We recommend starting with 0.5 and adjusting as needed.
Important: The API’s default certainty is 0.75 when omitted, which is stricter than the Playground’s 0.5. If you’re getting no results, add "certainty": 0.5 to your request to match Playground behavior.

Response Structure

Each result in the data array contains:
{
  "uuid": "unique-result-id",
  "metadata": {
    "distance": 0.396,
    "certainty": 0.802,
    "score": 0.0
  },
  "properties": {
    "item_title": "Finding True Happiness",
    "type": "Article",
    "author": ["Author Name"],
    "snippet": "Content text...",
    "summaries": { ... },
    "biblical_analysis": { ... }
  },
  "collection": "GlooProd"
}
Key fields:
  • metadata.certainty — Relevance score (0-1, higher = more relevant)
  • properties.snippet — Content chunk text, ideal for display or RAG context
  • properties.summaries — AI-generated summaries in multiple styles
  • properties.biblical_analysis — Bible references, concepts, and lessons (if applicable)

Test in the Playground First

Before writing code, test your queries in the Search Playground to verify your content is indexed and understand the response structure.
  1. Navigate to Playground in Gloo AI Studio
  2. Select the Search tab
  3. Choose your publisher from the dropdown
  4. Enter a query and review the results
Try it now: Search for a topic covered in your uploaded content. The Playground displays each result with its title, snippet text, and AI-generated insights.

Let’s make a search request with proper authentication. This is the foundation for everything that follows. Each implementation below handles token management, makes the search request, and displays results with titles, types, authors, and relevance scores.
#!/usr/bin/env python3
"""Basic search using the Gloo AI Search API."""

import requests
import os
import sys
import time
from dotenv import load_dotenv

load_dotenv()

# Configuration
CLIENT_ID = os.getenv("GLOO_CLIENT_ID", "YOUR_CLIENT_ID")
CLIENT_SECRET = os.getenv("GLOO_CLIENT_SECRET", "YOUR_CLIENT_SECRET")
TENANT = os.getenv("GLOO_TENANT", "your-tenant-name")
TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token"
SEARCH_URL = "https://platform.ai.gloo.com/ai/data/v1/search"

# --- Token Management ---

access_token_info = {}

def get_access_token():
    """Retrieve a new access token using OAuth2 client credentials."""
    global access_token_info
    response = requests.post(
        TOKEN_URL,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data={"grant_type": "client_credentials", "scope": "api/access"},
        auth=(CLIENT_ID, CLIENT_SECRET),
        timeout=30
    )
    response.raise_for_status()
    token_data = response.json()
    token_data['expires_at'] = int(time.time()) + token_data['expires_in']
    access_token_info = token_data
    return token_data

def ensure_valid_token():
    """Ensure we have a valid (non-expired) access token."""
    if not access_token_info or time.time() > (access_token_info.get('expires_at', 0) - 60):
        get_access_token()
    return access_token_info['access_token']

# --- Search ---

def search(query, limit=10):
    """Perform a semantic search query."""
    token = ensure_valid_token()

    payload = {
        "query": query,
        "collection": "GlooProd",
        "tenant": TENANT,
        "limit": limit,
        "certainty": 0.5
    }

    response = requests.post(
        SEARCH_URL,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        },
        json=payload,
        timeout=60
    )
    response.raise_for_status()
    return response.json()

# --- Run ---

query = sys.argv[1] if len(sys.argv) > 1 else "How can I know my purpose?"
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10

print(f"Searching for: '{query}'")
print(f"Limit: {limit} results\n")

results = search(query, limit)

if not results.get('data'):
    print("No results found.")
else:
    print(f"Found {len(results['data'])} results:\n")
    for i, result in enumerate(results['data'], 1):
        props = result.get('properties', {})
        meta = result.get('metadata', {})
        print(f"--- Result {i} ---")
        print(f"Title: {props.get('item_title', 'N/A')}")
        print(f"Type: {props.get('type', 'N/A')}")
        print(f"Author: {', '.join(props.get('author', ['N/A']))}")
        print(f"Relevance Score: {meta.get('certainty', 0):.4f}")
        snippet = props.get('snippet', '')
        if snippet:
            print(f"Snippet: {snippet[:200]}...")
        print()
/**
 * Basic search using the Gloo AI Search API.
 */

const axios = require('axios');
require('dotenv').config();

// Configuration
const CLIENT_ID = process.env.GLOO_CLIENT_ID || 'YOUR_CLIENT_ID';
const CLIENT_SECRET = process.env.GLOO_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/data/v1/search';

// --- Token Management ---

let tokenInfo = {};

async function getAccessToken() {
  const body = 'grant_type=client_credentials&scope=api/access';
  const response = await axios.post(TOKEN_URL, body, {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    auth: { username: CLIENT_ID, password: CLIENT_SECRET },
  });
  const tokenData = response.data;
  tokenData.expires_at = Math.floor(Date.now() / 1000) + tokenData.expires_in;
  tokenInfo = tokenData;
  return tokenData;
}

async function ensureValidToken() {
  if (!tokenInfo.expires_at || Date.now() / 1000 > tokenInfo.expires_at - 60) {
    await getAccessToken();
  }
  return tokenInfo.access_token;
}

// --- Search ---

async function search(query, limit = 10) {
  const token = await ensureValidToken();

  const response = await axios.post(
    SEARCH_URL,
    {
      query,
      collection: 'GlooProd',
      tenant: TENANT,
      limit,
      certainty: 0.5,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      timeout: 60000,
    },
  );

  return response.data;
}

// --- Run ---

(async () => {
  const query = process.argv[2] || 'How can I know my purpose?';
  const limit = parseInt(process.argv[3] || '10', 10);

  console.log(`Searching for: '${query}'`);
  console.log(`Limit: ${limit} results\n`);

  const results = await search(query, limit);

  if (!results.data || results.data.length === 0) {
    console.log('No results found.');
    return;
  }

  console.log(`Found ${results.data.length} results:\n`);

  results.data.forEach((result, i) => {
    const props = result.properties || {};
    const metadata = result.metadata || {};
    console.log(`--- Result ${i + 1} ---`);
    console.log(`Title: ${props.item_title || 'N/A'}`);
    console.log(`Type: ${props.type || 'N/A'}`);
    console.log(`Author: ${(props.author || ['N/A']).join(', ')}`);
    console.log(`Relevance Score: ${(metadata.certainty || 0).toFixed(4)}`);
    if (props.snippet) {
      console.log(`Snippet: ${props.snippet.substring(0, 200)}...`);
    }
    console.log();
  });
})();
/**
 * Basic search using the Gloo AI Search API.
 */

import axios from 'axios';
import dotenv from 'dotenv';

dotenv.config();

// Configuration
const CLIENT_ID = process.env.GLOO_CLIENT_ID || 'YOUR_CLIENT_ID';
const CLIENT_SECRET = process.env.GLOO_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/data/v1/search';

// --- Types ---

interface SearchResult {
  uuid: string;
  metadata: { distance: number; certainty: number; score: number };
  properties: {
    item_title: string;
    type: string;
    author: string[];
    snippet: string;
  };
  collection: string;
}

interface SearchResponse {
  data: SearchResult[];
  intent: number;
}

// --- Token Management ---

let tokenInfo: Record<string, any> = {};

async function getAccessToken() {
  const body = 'grant_type=client_credentials&scope=api/access';
  const response = await axios.post(TOKEN_URL, body, {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    auth: { username: CLIENT_ID, password: CLIENT_SECRET },
  });
  const tokenData = response.data;
  tokenData.expires_at = Math.floor(Date.now() / 1000) + tokenData.expires_in;
  tokenInfo = tokenData;
  return tokenData;
}

async function ensureValidToken(): Promise<string> {
  if (!tokenInfo.expires_at || Date.now() / 1000 > tokenInfo.expires_at - 60) {
    await getAccessToken();
  }
  return tokenInfo.access_token;
}

// --- Search ---

async function search(
  query: string,
  limit: number = 10,
): Promise<SearchResponse> {
  const token = await ensureValidToken();

  const response = await axios.post<SearchResponse>(
    SEARCH_URL,
    {
      query,
      collection: 'GlooProd',
      tenant: TENANT,
      limit,
      certainty: 0.5,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      timeout: 60000,
    },
  );

  return response.data;
}

// --- Run ---

(async () => {
  const query = process.argv[2] || 'How can I know my purpose?';
  const limit = parseInt(process.argv[3] || '10', 10);

  console.log(`Searching for: '${query}'`);
  console.log(`Limit: ${limit} results\n`);

  const results = await search(query, limit);

  if (!results.data || results.data.length === 0) {
    console.log('No results found.');
  } else {
    console.log(`Found ${results.data.length} results:\n`);
    results.data.forEach((result, i) => {
      console.log(`--- Result ${i + 1} ---`);
      console.log(`Title: ${result.properties.item_title || 'N/A'}`);
      console.log(`Type: ${result.properties.type || 'N/A'}`);
      console.log(`Author: ${(result.properties.author || ['N/A']).join(', ')}`);
      console.log(
        `Relevance Score: ${(result.metadata.certainty || 0).toFixed(4)}`,
      );
      if (result.properties.snippet) {
        console.log(`Snippet: ${result.properties.snippet.substring(0, 200)}...`);
      }
      console.log();
    });
  }
})();
<?php
/**
 * Basic search using the Gloo AI Search API.
 */

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';
use Dotenv\Dotenv;

$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();

// Configuration
$CLIENT_ID = $_ENV['GLOO_CLIENT_ID'] ?? 'YOUR_CLIENT_ID';
$CLIENT_SECRET = $_ENV['GLOO_CLIENT_SECRET'] ?? 'YOUR_CLIENT_SECRET';
$TENANT = $_ENV['GLOO_TENANT'] ?? 'your-tenant-name';
$TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token';
$SEARCH_URL = 'https://platform.ai.gloo.com/ai/data/v1/search';

// --- Token Management ---

$tokenInfo = [];

function ensureValidToken(): string {
    global $tokenInfo, $CLIENT_ID, $CLIENT_SECRET, $TOKEN_URL;

    if (empty($tokenInfo['expires_at']) || time() > $tokenInfo['expires_at'] - 60) {
        $ch = curl_init($TOKEN_URL);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials&scope=api/access');
        curl_setopt($ch, CURLOPT_USERPWD, $CLIENT_ID . ':' . $CLIENT_SECRET);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);

        $result = curl_exec($ch);
        curl_close($ch);

        $tokenInfo = json_decode($result, true);
        $tokenInfo['expires_at'] = time() + $tokenInfo['expires_in'];
    }

    return $tokenInfo['access_token'];
}

// --- Search ---

function search(string $query, int $limit = 10): array {
    global $TENANT, $SEARCH_URL;

    $token = ensureValidToken();
    $payload = json_encode([
        'query' => $query,
        'collection' => 'GlooProd',
        'tenant' => $TENANT,
        'limit' => $limit,
        'certainty' => 0.5,
    ]);

    $ch = curl_init($SEARCH_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);

    $result = curl_exec($ch);
    curl_close($ch);

    return json_decode($result, true);
}

// --- Run ---

$query = $argv[1] ?? 'How can I know my purpose?';
$limit = isset($argv[2]) ? (int)$argv[2] : 10;

echo "Searching for: '$query'\n";
echo "Limit: $limit results\n\n";

$results = search($query, $limit);

if (empty($results['data'])) {
    echo "No results found.\n";
} else {
    echo "Found " . count($results['data']) . " results:\n\n";
    foreach ($results['data'] as $i => $result) {
        $props = $result['properties'] ?? [];
        $meta = $result['metadata'] ?? [];
        echo "--- Result " . ($i + 1) . " ---\n";
        echo "Title: " . ($props['item_title'] ?? 'N/A') . "\n";
        echo "Type: " . ($props['type'] ?? 'N/A') . "\n";
        echo "Author: " . implode(', ', $props['author'] ?? ['N/A']) . "\n";
        echo "Relevance Score: " . number_format($meta['certainty'] ?? 0, 4) . "\n";
        if (!empty($props['snippet'])) {
            echo "Snippet: " . substr($props['snippet'], 0, 200) . "...\n";
        }
        echo "\n";
    }
}
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/joho/godotenv"
)

// Configuration loaded from .env
var (
    clientID, clientSecret, tenant string
    tokenURL   = "https://platform.ai.gloo.com/oauth2/token"
    searchURL  = "https://platform.ai.gloo.com/ai/data/v1/search"
    tokenInfo  map[string]interface{}
)

// --- Types ---

type SearchRequest struct {
    Query      string  `json:"query"`
    Collection string  `json:"collection"`
    Tenant     string  `json:"tenant"`
    Limit      int     `json:"limit"`
    Certainty  float64 `json:"certainty"`
}

type SearchResult struct {
    UUID       string `json:"uuid"`
    Metadata   struct {
        Certainty float64 `json:"certainty"`
    } `json:"metadata"`
    Properties struct {
        ItemTitle string   `json:"item_title"`
        Type      string   `json:"type"`
        Author    []string `json:"author"`
        Snippet   string   `json:"snippet"`
    } `json:"properties"`
}

type SearchResponse struct {
    Data []SearchResult `json:"data"`
}

// --- Token Management ---

func ensureValidToken() string {
    expiresAt, _ := tokenInfo["expires_at"].(float64)
    if tokenInfo == nil || float64(time.Now().Unix()) > expiresAt-60 {
        form := url.Values{"grant_type": {"client_credentials"}, "scope": {"api/access"}}
        req, _ := http.NewRequest("POST", tokenURL, bytes.NewBufferString(form.Encode()))
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
        req.SetBasicAuth(clientID, clientSecret)

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Token request failed: %v\n", err)
            os.Exit(1)
        }
        defer resp.Body.Close()
        if err := json.NewDecoder(resp.Body).Decode(&tokenInfo); err != nil {
            fmt.Fprintf(os.Stderr, "Token decode failed: %v\n", err)
            os.Exit(1)
        }
        tokenInfo["expires_at"] = float64(time.Now().Unix()) + tokenInfo["expires_in"].(float64)
    }
    return tokenInfo["access_token"].(string)
}

// --- Search ---

func search(query string, limit int) (*SearchResponse, error) {
    token := ensureValidToken()

    payload, _ := json.Marshal(SearchRequest{
        Query: query, Collection: "GlooProd",
        Tenant: tenant, Limit: limit, Certainty: 0.5,
    })

    req, _ := http.NewRequest("POST", searchURL, bytes.NewBuffer(payload))
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Content-Type", "application/json")

    resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        body, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("search failed: %d %s", resp.StatusCode, string(body))
    }

    var result SearchResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decode failed: %w", err)
    }
    return &result, nil
}

// --- Run ---

func main() {
    godotenv.Load()
    clientID = os.Getenv("GLOO_CLIENT_ID")
    clientSecret = os.Getenv("GLOO_CLIENT_SECRET")
    tenant = os.Getenv("GLOO_TENANT")
    tokenInfo = map[string]interface{}{}

    query := "How can I know my purpose?"
    limit := 10
    if len(os.Args) > 2 { query = os.Args[2] }
    if len(os.Args) > 3 { limit, _ = strconv.Atoi(os.Args[3]) }

    fmt.Printf("Searching for: '%s'\nLimit: %d results\n\n", query, limit)

    results, err := search(query, limit)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Search failed: %v\n", err)
        os.Exit(1)
    }

    if len(results.Data) == 0 {
        fmt.Println("No results found.")
        return
    }

    fmt.Printf("Found %d results:\n\n", len(results.Data))
    for i, r := range results.Data {
        fmt.Printf("--- Result %d ---\n", i+1)
        fmt.Printf("Title: %s\n", r.Properties.ItemTitle)
        fmt.Printf("Type: %s\n", r.Properties.Type)
        fmt.Printf("Author: %s\n", strings.Join(r.Properties.Author, ", "))
        fmt.Printf("Relevance Score: %.4f\n", r.Metadata.Certainty)
        snippet := r.Properties.Snippet
        if len(snippet) > 200 { snippet = snippet[:200] }
        if snippet != "" { fmt.Printf("Snippet: %s...\n", snippet) }
        fmt.Println()
    }
}
import com.google.gson.Gson;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;

public class SearchExample {

    static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
    static final String CLIENT_ID = dotenv.get("GLOO_CLIENT_ID");
    static final String CLIENT_SECRET = dotenv.get("GLOO_CLIENT_SECRET");
    static final String TENANT = dotenv.get("GLOO_TENANT");
    static final String TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token";
    static final String SEARCH_URL = "https://platform.ai.gloo.com/ai/data/v1/search";
    static final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30)).build();
    static final Gson gson = new Gson();
    static Map<String, Object> tokenInfo = new HashMap<>();

    // --- Token Management ---

    static String ensureValidToken() throws Exception {
        double expiresAt = tokenInfo.containsKey("expires_at")
                ? ((Number)tokenInfo.get("expires_at")).doubleValue() : 0;
        if (System.currentTimeMillis() / 1000.0 > expiresAt - 60) {
            String auth = Base64.getEncoder().encodeToString(
                    (CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
            HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(TOKEN_URL))
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("Authorization", "Basic " + auth)
                    .POST(HttpRequest.BodyPublishers.ofString(
                            "grant_type=client_credentials&scope=api/access"))
                    .build();
            HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
            tokenInfo = gson.fromJson(resp.body(), Map.class);
            tokenInfo.put("expires_at",
                    System.currentTimeMillis() / 1000.0 + ((Number)tokenInfo.get("expires_in")).doubleValue());
        }
        return (String) tokenInfo.get("access_token");
    }

    // --- Search ---

    static Map search(String query, int limit) throws Exception {
        String token = ensureValidToken();
        Map<String, Object> payload = Map.of(
                "query", query, "collection", "GlooProd",
                "tenant", TENANT, "limit", limit, "certainty", 0.5);

        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(SEARCH_URL))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
                .timeout(Duration.ofSeconds(60))
                .build();

        HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
        return gson.fromJson(resp.body(), Map.class);
    }

    // --- Run ---

    public static void main(String[] args) throws Exception {
        String query = args.length > 0 ? args[0] : "How can I know my purpose?";
        int limit = args.length > 1 ? Integer.parseInt(args[1]) : 10;

        System.out.printf("Searching for: '%s'%nLimit: %d results%n%n", query, limit);

        Map results = search(query, limit);
        List<Map> data = (List<Map>) results.get("data");

        if (data == null || data.isEmpty()) {
            System.out.println("No results found.");
            return;
        }

        System.out.printf("Found %d results:%n%n", data.size());
        for (int i = 0; i < data.size(); i++) {
            Map props = (Map) data.get(i).get("properties");
            Map meta = (Map) data.get(i).get("metadata");
            System.out.printf("--- Result %d ---%n", i + 1);
            System.out.printf("Title: %s%n", props.get("item_title"));
            System.out.printf("Type: %s%n", props.get("type"));
            System.out.printf("Author: %s%n",
                    String.join(", ", (List<String>) props.getOrDefault("author", List.of("N/A"))));
            System.out.printf("Relevance Score: %.4f%n", (Double) meta.get("certainty"));
            String snippet = (String) props.getOrDefault("snippet", "");
            if (!snippet.isEmpty()) {
                System.out.printf("Snippet: %s...%n",
                        snippet.length() > 200 ? snippet.substring(0, 200) : snippet);
            }
            System.out.println();
        }
    }
}

What You’ll See

A successful search returns results with titles, types, and relevance scores:
Searching for: 'How can I know my purpose?'
Limit: 10 results

Found 9 results:

--- Result 1 ---
Title: Finding True Happiness
Type: Article
Author: Automated Ingestion
Relevance Score: 0.7920
Snippet: # Finding True Happiness: A Christian Perspective  In a world obsessed...

--- Result 2 ---
Title: Finding True Happiness
Type: Article
Author: Automated Ingestion
Relevance Score: 0.7700
Snippet: ## The Beatitudes: God's Blueprint for Blessedness Jesus outlined the path...

Key Points

  • Collection must always be "GlooProd"
  • Tenant scopes results to your publisher’s content only
  • certainty: 0.5 matches the Playground default — adjust as needed
  • Request time increases non-linearly with larger limit values

Run the Cookbook Example

The cookbook includes a ready-to-run basic search script for each language:
cd search-tutorial/python
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt
python search_basic.py "How can I know my purpose?" 5
cd search-tutorial/javascript
npm install
node search-basic.js "How can I know my purpose?" 5
cd search-tutorial/typescript
npm install
npx ts-node search-basic.ts "How can I know my purpose?" 5
cd search-tutorial/php
composer install
php search_basic.php "How can I know my purpose?" 5
cd search-tutorial/go
go mod download
go run . search "How can I know my purpose?" 5
cd search-tutorial/java
mvn clean install -q
mvn exec:java -Dexec.args='search "How can I know my purpose?" 5'

Step 2: Search + RAG with Completions V2

Search results become even more powerful when used as context for AI-generated responses. This is Retrieval Augmented Generation (RAG) — search for relevant content, then generate an answer grounded in that content.
Two approaches to RAG with Gloo AI:
  • Search API + Completions V2 (this section): Full control over context, prompts, and formatting
  • Grounded Completions: Single API call, simpler but less control
Both retrieve identical content. The difference is who controls how that content is presented to the LLM.

The RAG Workflow

  1. Search — Query the Search API for relevant content
  2. Extract — Pull snippets from results
  3. Format — Build context for the LLM
  4. Generate — Call Completions V2 with the context
  5. Return — Deliver the response with source citations
#!/usr/bin/env python3
"""Search + RAG using the Gloo AI Search API and Completions V2."""

import requests
import os
import sys
import time
from dotenv import load_dotenv

load_dotenv()

# Configuration
CLIENT_ID = os.getenv("GLOO_CLIENT_ID", "YOUR_CLIENT_ID")
CLIENT_SECRET = os.getenv("GLOO_CLIENT_SECRET", "YOUR_CLIENT_SECRET")
TENANT = os.getenv("GLOO_TENANT", "your-tenant-name")
TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token"
SEARCH_URL = "https://platform.ai.gloo.com/ai/data/v1/search"
COMPLETIONS_URL = "https://platform.ai.gloo.com/ai/v2/chat/completions"

# --- Token Management (same as Step 1) ---

access_token_info = {}

def ensure_valid_token():
    global access_token_info
    if not access_token_info or time.time() > (access_token_info.get('expires_at', 0) - 60):
        response = requests.post(
            TOKEN_URL,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            data={"grant_type": "client_credentials", "scope": "api/access"},
            auth=(CLIENT_ID, CLIENT_SECRET), timeout=30
        )
        response.raise_for_status()
        access_token_info = response.json()
        access_token_info['expires_at'] = int(time.time()) + access_token_info['expires_in']
    return access_token_info['access_token']

# --- Step 1: Search ---

def search(query, limit=5):
    token = ensure_valid_token()
    response = requests.post(SEARCH_URL, headers={
        "Authorization": f"Bearer {token}", "Content-Type": "application/json"
    }, json={
        "query": query, "collection": "GlooProd",
        "tenant": TENANT, "limit": limit, "certainty": 0.5
    }, timeout=60)
    response.raise_for_status()
    return response.json()

# --- Step 2: Extract Snippets ---

def extract_snippets(results, max_snippets=5, max_chars=500):
    snippets = []
    for result in results.get("data", [])[:max_snippets]:
        props = result.get("properties", {})
        snippets.append({
            "text": props.get("snippet", "")[:max_chars],
            "title": props.get("item_title", "N/A"),
            "type": props.get("type", "N/A"),
        })
    return snippets

# --- Step 3: Format Context ---

def format_context(snippets):
    parts = []
    for i, s in enumerate(snippets, 1):
        parts.append(f"[Source {i}: {s['title']} ({s['type']})]\n{s['text']}\n")
    return "\n---\n".join(parts)

# --- Step 4: Generate Response ---

def generate_with_context(query, context):
    token = ensure_valid_token()
    payload = {
        "messages": [
            {"role": "system", "content":
                "You are a helpful assistant. Answer the user's question based on the "
                "provided context. If the context doesn't contain relevant information, "
                "say so honestly."},
            {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"}
        ],
        "auto_routing": True,
        "max_tokens": 3000
    }
    response = requests.post(COMPLETIONS_URL, headers={
        "Authorization": f"Bearer {token}", "Content-Type": "application/json"
    }, json=payload, timeout=60)
    response.raise_for_status()
    return response.json()["choices"][0]["message"]["content"]

# --- Run Complete RAG Flow ---

query = sys.argv[2] if len(sys.argv) > 2 else "How can I know my purpose?"
limit = int(sys.argv[3]) if len(sys.argv) > 3 else 5

print(f"RAG Search for: '{query}'\n")

print("Step 1: Searching for relevant content...")
results = search(query, limit)
print(f"Found {len(results.get('data', []))} results\n")

print("Step 2: Extracting snippets...")
snippets = extract_snippets(results)
context = format_context(snippets)
print(f"Extracted {len(snippets)} snippets\n")

print("Step 3: Generating response with context...\n")
response = generate_with_context(query, context)

print("=== Generated Response ===")
print(response)
print("\n=== Sources Used ===")
for s in snippets:
    print(f"- {s['title']} ({s['type']})")
/**
 * Search + RAG using the Gloo AI Search API and Completions V2.
 */

const axios = require('axios');
require('dotenv').config();

// Configuration
const CLIENT_ID = process.env.GLOO_CLIENT_ID || 'YOUR_CLIENT_ID';
const CLIENT_SECRET = process.env.GLOO_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/data/v1/search';
const COMPLETIONS_URL = 'https://platform.ai.gloo.com/ai/v2/chat/completions';

// --- Token Management (same as Step 1) ---

let tokenInfo = {};

async function ensureValidToken() {
  if (!tokenInfo.expires_at || Date.now() / 1000 > tokenInfo.expires_at - 60) {
    const response = await axios.post(
      TOKEN_URL,
      'grant_type=client_credentials&scope=api/access',
      {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        auth: { username: CLIENT_ID, password: CLIENT_SECRET },
      },
    );
    tokenInfo = response.data;
    tokenInfo.expires_at = Math.floor(Date.now() / 1000) + tokenInfo.expires_in;
  }
  return tokenInfo.access_token;
}

// --- Step 1: Search ---

async function search(query, limit = 5) {
  const token = await ensureValidToken();
  const response = await axios.post(
    SEARCH_URL,
    {
      query,
      collection: 'GlooProd',
      tenant: TENANT,
      limit,
      certainty: 0.5,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      timeout: 60000,
    },
  );
  return response.data;
}

// --- Step 2: Extract Snippets ---

function extractSnippets(results, maxSnippets = 5, maxChars = 500) {
  return (results.data || []).slice(0, maxSnippets).map((r) => ({
    text: (r.properties.snippet || '').substring(0, maxChars),
    title: r.properties.item_title || 'N/A',
    type: r.properties.type || 'N/A',
  }));
}

// --- Step 3: Format Context ---

function formatContext(snippets) {
  return snippets
    .map((s, i) => `[Source ${i + 1}: ${s.title} (${s.type})]\n${s.text}\n`)
    .join('\n---\n');
}

// --- Step 4: Generate Response ---

async function generateWithContext(query, context) {
  const token = await ensureValidToken();
  const response = await axios.post(
    COMPLETIONS_URL,
    {
      messages: [
        {
          role: 'system',
          content:
            "You are a helpful assistant. Answer the user's question based on the " +
            "provided context. If the context doesn't contain relevant information, " +
            'say so honestly.',
        },
        { role: 'user', content: `Context:\n${context}\n\nQuestion: ${query}` },
      ],
      auto_routing: true,
      max_tokens: 3000,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      timeout: 60000,
    },
  );
  return response.data.choices[0].message.content;
}

// --- Run Complete RAG Flow ---

(async () => {
  const query = process.argv[3] || 'How can I know my purpose?';
  const limit = parseInt(process.argv[4] || '5', 10);

  console.log(`RAG Search for: '${query}'\n`);

  console.log('Step 1: Searching for relevant content...');
  const results = await search(query, limit);
  console.log(`Found ${results.data?.length || 0} results\n`);

  console.log('Step 2: Extracting snippets...');
  const snippets = extractSnippets(results);
  const context = formatContext(snippets);
  console.log(`Extracted ${snippets.length} snippets\n`);

  console.log('Step 3: Generating response with context...\n');
  const response = await generateWithContext(query, context);

  console.log('=== Generated Response ===');
  console.log(response);
  console.log('\n=== Sources Used ===');
  snippets.forEach((s) => console.log(`- ${s.title} (${s.type})`));
})();
/**
 * Search + RAG using the Gloo AI Search API and Completions V2.
 */

import axios from 'axios';
import dotenv from 'dotenv';

dotenv.config();

// Configuration
const CLIENT_ID = process.env.GLOO_CLIENT_ID || 'YOUR_CLIENT_ID';
const CLIENT_SECRET = process.env.GLOO_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
const TENANT = process.env.GLOO_TENANT || 'your-tenant-name';
const TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token';
const SEARCH_URL = 'https://platform.ai.gloo.com/ai/data/v1/search';
const COMPLETIONS_URL = 'https://platform.ai.gloo.com/ai/v2/chat/completions';

// --- Token Management (same as Step 1) ---

let tokenInfo: Record<string, any> = {};

async function ensureValidToken(): Promise<string> {
  if (!tokenInfo.expires_at || Date.now() / 1000 > tokenInfo.expires_at - 60) {
    const response = await axios.post(
      TOKEN_URL,
      'grant_type=client_credentials&scope=api/access',
      {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        auth: { username: CLIENT_ID, password: CLIENT_SECRET },
      },
    );
    tokenInfo = response.data;
    tokenInfo.expires_at = Math.floor(Date.now() / 1000) + tokenInfo.expires_in;
  }
  return tokenInfo.access_token;
}

// --- Step 1: Search ---

interface SearchResult {
  metadata: { certainty: number };
  properties: { item_title: string; type: string; snippet: string };
}

async function search(query: string, limit = 5) {
  const token = await ensureValidToken();
  const response = await axios.post(
    SEARCH_URL,
    {
      query,
      collection: 'GlooProd',
      tenant: TENANT,
      limit,
      certainty: 0.5,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      timeout: 60000,
    },
  );
  return response.data as { data: SearchResult[] };
}

// --- Steps 2-3: Extract & Format ---

interface Snippet {
  text: string;
  title: string;
  type: string;
}

function extractSnippets(
  results: { data: SearchResult[] },
  max = 5,
  maxChars = 500,
): Snippet[] {
  return (results.data || []).slice(0, max).map((r) => ({
    text: (r.properties.snippet || '').substring(0, maxChars),
    title: r.properties.item_title || 'N/A',
    type: r.properties.type || 'N/A',
  }));
}

function formatContext(snippets: Snippet[]): string {
  return snippets
    .map((s, i) => `[Source ${i + 1}: ${s.title} (${s.type})]\n${s.text}\n`)
    .join('\n---\n');
}

// --- Step 4: Generate Response ---

async function generateWithContext(
  query: string,
  context: string,
): Promise<string> {
  const token = await ensureValidToken();
  const response = await axios.post(
    COMPLETIONS_URL,
    {
      messages: [
        {
          role: 'system',
          content:
            "You are a helpful assistant. Answer the user's question based on the " +
            "provided context. If the context doesn't contain relevant information, " +
            'say so honestly.',
        },
        { role: 'user', content: `Context:\n${context}\n\nQuestion: ${query}` },
      ],
      auto_routing: true,
      max_tokens: 3000,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      timeout: 60000,
    },
  );
  return response.data.choices[0].message.content;
}

// --- Run Complete RAG Flow ---

(async () => {
  const query = process.argv[3] || 'How can I know my purpose?';
  const limit = parseInt(process.argv[4] || '5', 10);

  console.log(`RAG Search for: '${query}'\n`);

  console.log('Step 1: Searching for relevant content...');
  const results = await search(query, limit);
  console.log(`Found ${results.data?.length || 0} results\n`);

  console.log('Step 2: Extracting snippets...');
  const snippets = extractSnippets(results);
  const context = formatContext(snippets);
  console.log(`Extracted ${snippets.length} snippets\n`);

  console.log('Step 3: Generating response with context...\n');
  const answer = await generateWithContext(query, context);

  console.log('=== Generated Response ===');
  console.log(answer);
  console.log('\n=== Sources Used ===');
  snippets.forEach((s) => console.log(`- ${s.title} (${s.type})`));
})();
<?php
/**
 * Search + RAG using the Gloo AI Search API and Completions V2.
 */

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';
use Dotenv\Dotenv;

$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();

// Configuration
$CLIENT_ID = $_ENV['GLOO_CLIENT_ID'] ?? 'YOUR_CLIENT_ID';
$CLIENT_SECRET = $_ENV['GLOO_CLIENT_SECRET'] ?? 'YOUR_CLIENT_SECRET';
$TENANT = $_ENV['GLOO_TENANT'] ?? 'your-tenant-name';
$TOKEN_URL = 'https://platform.ai.gloo.com/oauth2/token';
$SEARCH_URL = 'https://platform.ai.gloo.com/ai/data/v1/search';
$COMPLETIONS_URL = 'https://platform.ai.gloo.com/ai/v2/chat/completions';

// --- Token Management (same as Step 1) ---

$tokenInfo = [];

function ensureValidToken(): string {
    global $tokenInfo, $CLIENT_ID, $CLIENT_SECRET, $TOKEN_URL;
    if (empty($tokenInfo['expires_at']) || time() > $tokenInfo['expires_at'] - 60) {
        $ch = curl_init($TOKEN_URL);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials&scope=api/access');
        curl_setopt($ch, CURLOPT_USERPWD, $CLIENT_ID . ':' . $CLIENT_SECRET);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
        $result = curl_exec($ch);
        curl_close($ch);
        $tokenInfo = json_decode($result, true);
        $tokenInfo['expires_at'] = time() + $tokenInfo['expires_in'];
    }
    return $tokenInfo['access_token'];
}

// --- Step 1: Search ---

function search(string $query, int $limit = 5): array {
    global $TENANT, $SEARCH_URL;
    $token = ensureValidToken();
    $ch = curl_init($SEARCH_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        'query' => $query, 'collection' => 'GlooProd',
        'tenant' => $TENANT, 'limit' => $limit, 'certainty' => 0.5,
    ]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $token, 'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    $result = curl_exec($ch);
    curl_close($ch);
    return json_decode($result, true);
}

// --- Steps 2-3: Extract & Format ---

function extractSnippets(array $results, int $max = 5, int $maxChars = 500): array {
    $snippets = [];
    foreach (array_slice($results['data'] ?? [], 0, $max) as $result) {
        $props = $result['properties'] ?? [];
        $snippets[] = [
            'text' => substr($props['snippet'] ?? '', 0, $maxChars),
            'title' => $props['item_title'] ?? 'N/A',
            'type' => $props['type'] ?? 'N/A',
        ];
    }
    return $snippets;
}

function formatContext(array $snippets): string {
    $parts = [];
    foreach ($snippets as $i => $s) {
        $num = $i + 1;
        $parts[] = "[Source $num: {$s['title']} ({$s['type']})]\n{$s['text']}\n";
    }
    return implode("\n---\n", $parts);
}

// --- Step 4: Generate Response ---

function generateWithContext(string $query, string $context): string {
    global $COMPLETIONS_URL;
    $token = ensureValidToken();
    $ch = curl_init($COMPLETIONS_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        'messages' => [
            ['role' => 'system', 'content' =>
                'You are a helpful assistant. Answer the user\'s question based on the '
                . 'provided context. If the context doesn\'t contain relevant information, '
                . 'say so honestly.'],
            ['role' => 'user', 'content' => "Context:\n$context\n\nQuestion: $query"],
        ],
        'auto_routing' => true,
        'max_tokens' => 3000,
    ]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $token, 'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    $result = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($result, true);
    return $data['choices'][0]['message']['content'] ?? '';
}

// --- Run Complete RAG Flow ---

$query = $argv[2] ?? 'How can I know my purpose?';
$limit = isset($argv[3]) ? (int)$argv[3] : 5;

echo "RAG Search for: '$query'\n\n";

echo "Step 1: Searching for relevant content...\n";
$results = search($query, $limit);
echo "Found " . count($results['data'] ?? []) . " results\n\n";

echo "Step 2: Extracting snippets...\n";
$snippets = extractSnippets($results);
$context = formatContext($snippets);
echo "Extracted " . count($snippets) . " snippets\n\n";

echo "Step 3: Generating response with context...\n\n";
$response = generateWithContext($query, $context);

echo "=== Generated Response ===\n";
echo $response . "\n";
echo "\n=== Sources Used ===\n";
foreach ($snippets as $s) {
    echo "- {$s['title']} ({$s['type']})\n";
}
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"

    "github.com/joho/godotenv"
)

var (
    clientID, clientSecret, tenant string
    tokenURL       = "https://platform.ai.gloo.com/oauth2/token"
    searchURL      = "https://platform.ai.gloo.com/ai/data/v1/search"
    completionsURL = "https://platform.ai.gloo.com/ai/v2/chat/completions"
    tokenInfo      map[string]interface{}
)

type Snippet struct{ Text, Title, Type string }

func ensureValidToken() string {
    expiresAt, _ := tokenInfo["expires_at"].(float64)
    if tokenInfo == nil || float64(time.Now().Unix()) > expiresAt-60 {
        form := url.Values{"grant_type": {"client_credentials"}, "scope": {"api/access"}}
        req, _ := http.NewRequest("POST", tokenURL, bytes.NewBufferString(form.Encode()))
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
        req.SetBasicAuth(clientID, clientSecret)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Token request failed: %v\n", err)
            os.Exit(1)
        }
        defer resp.Body.Close()
        if err := json.NewDecoder(resp.Body).Decode(&tokenInfo); err != nil {
            fmt.Fprintf(os.Stderr, "Token decode failed: %v\n", err)
            os.Exit(1)
        }
        tokenInfo["expires_at"] = float64(time.Now().Unix()) + tokenInfo["expires_in"].(float64)
    }
    return tokenInfo["access_token"].(string)
}

func search(query string, limit int) map[string]interface{} {
    token := ensureValidToken()
    payload, _ := json.Marshal(map[string]interface{}{
        "query": query, "collection": "GlooProd",
        "tenant": tenant, "limit": limit, "certainty": 0.5,
    })
    req, _ := http.NewRequest("POST", searchURL, bytes.NewBuffer(payload))
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Content-Type", "application/json")
    resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Search request failed: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    var result map[string]interface{}
    if err := json.Unmarshal(body, &result); err != nil {
        fmt.Fprintf(os.Stderr, "Search decode failed: %v\n", err)
        os.Exit(1)
    }
    return result
}

func extractSnippets(results map[string]interface{}, max, maxChars int) []Snippet {
    data, _ := results["data"].([]interface{})
    var snippets []Snippet
    for i, item := range data {
        if i >= max { break }
        r := item.(map[string]interface{})
        props := r["properties"].(map[string]interface{})
        text, _ := props["snippet"].(string)
        if len(text) > maxChars { text = text[:maxChars] }
        snippets = append(snippets, Snippet{
            Text: text, Title: fmt.Sprint(props["item_title"]), Type: fmt.Sprint(props["type"]),
        })
    }
    return snippets
}

func formatContext(snippets []Snippet) string {
    var parts []string
    for i, s := range snippets {
        parts = append(parts, fmt.Sprintf("[Source %d: %s (%s)]\n%s\n", i+1, s.Title, s.Type, s.Text))
    }
    result := ""
    for i, p := range parts {
        if i > 0 { result += "\n---\n" }
        result += p
    }
    return result
}

func generateWithContext(query, context string) string {
    token := ensureValidToken()
    payload, _ := json.Marshal(map[string]interface{}{
        "messages": []map[string]string{
            {"role": "system", "content": "You are a helpful assistant. Answer the user's question based on the provided context. If the context doesn't contain relevant information, say so honestly."},
            {"role": "user", "content": fmt.Sprintf("Context:\n%s\n\nQuestion: %s", context, query)},
        },
        "auto_routing": true, "max_tokens": 3000,
    })
    req, _ := http.NewRequest("POST", completionsURL, bytes.NewBuffer(payload))
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Content-Type", "application/json")
    resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Completions request failed: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    var result map[string]interface{}
    if err := json.Unmarshal(body, &result); err != nil {
        fmt.Fprintf(os.Stderr, "Completions decode failed: %v\n", err)
        os.Exit(1)
    }
    choices := result["choices"].([]interface{})
    msg := choices[0].(map[string]interface{})["message"].(map[string]interface{})
    return msg["content"].(string)
}

func main() {
    godotenv.Load()
    clientID = os.Getenv("GLOO_CLIENT_ID")
    clientSecret = os.Getenv("GLOO_CLIENT_SECRET")
    tenant = os.Getenv("GLOO_TENANT")
    tokenInfo = map[string]interface{}{}

    query := "How can I know my purpose?"
    limit := 5
    if len(os.Args) > 2 { query = os.Args[2] }
    if len(os.Args) > 3 { limit, _ = strconv.Atoi(os.Args[3]) }

    fmt.Printf("RAG Search for: '%s'\n\n", query)
    fmt.Println("Step 1: Searching for relevant content...")
    results := search(query, limit)
    data, _ := results["data"].([]interface{})
    fmt.Printf("Found %d results\n\n", len(data))

    fmt.Println("Step 2: Extracting snippets...")
    snippets := extractSnippets(results, 5, 500)
    context := formatContext(snippets)
    fmt.Printf("Extracted %d snippets\n\n", len(snippets))

    fmt.Println("Step 3: Generating response with context...\n")
    response := generateWithContext(query, context)

    fmt.Println("=== Generated Response ===")
    fmt.Println(response)
    fmt.Println("\n=== Sources Used ===")
    for _, s := range snippets { fmt.Printf("- %s (%s)\n", s.Title, s.Type) }
}
import com.google.gson.Gson;
import io.github.cdimascio.dotenv.Dotenv;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;

public class RAGSearchExample {
    static final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
    static final String CLIENT_ID = dotenv.get("GLOO_CLIENT_ID");
    static final String CLIENT_SECRET = dotenv.get("GLOO_CLIENT_SECRET");
    static final String TENANT = dotenv.get("GLOO_TENANT");
    static final String TOKEN_URL = "https://platform.ai.gloo.com/oauth2/token";
    static final String SEARCH_URL = "https://platform.ai.gloo.com/ai/data/v1/search";
    static final String COMPLETIONS_URL = "https://platform.ai.gloo.com/ai/v2/chat/completions";
    static final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30)).build();
    static final Gson gson = new Gson();
    static Map<String, Object> tokenInfo = new HashMap<>();

    static String ensureValidToken() throws Exception {
        double expiresAt = tokenInfo.containsKey("expires_at")
                ? ((Number)tokenInfo.get("expires_at")).doubleValue() : 0;
        if (System.currentTimeMillis() / 1000.0 > expiresAt - 60) {
            String auth = Base64.getEncoder().encodeToString(
                    (CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
            HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(TOKEN_URL))
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("Authorization", "Basic " + auth)
                    .POST(HttpRequest.BodyPublishers.ofString(
                            "grant_type=client_credentials&scope=api/access"))
                    .build();
            HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
            tokenInfo = gson.fromJson(resp.body(), Map.class);
            tokenInfo.put("expires_at",
                    System.currentTimeMillis() / 1000.0 + ((Number)tokenInfo.get("expires_in")).doubleValue());
        }
        return (String) tokenInfo.get("access_token");
    }

    static Map search(String query, int limit) throws Exception {
        String token = ensureValidToken();
        Map<String, Object> payload = Map.of(
                "query", query, "collection", "GlooProd",
                "tenant", TENANT, "limit", limit, "certainty", 0.5);
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(SEARCH_URL))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
                .timeout(Duration.ofSeconds(60)).build();
        HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
        return gson.fromJson(resp.body(), Map.class);
    }

    static List<Map<String, String>> extractSnippets(Map results, int max, int maxChars) {
        List<Map> data = (List<Map>) results.get("data");
        if (data == null) return List.of();
        List<Map<String, String>> snippets = new ArrayList<>();
        for (int i = 0; i < Math.min(data.size(), max); i++) {
            Map props = (Map) data.get(i).get("properties");
            String text = (String) props.getOrDefault("snippet", "");
            if (text.length() > maxChars) text = text.substring(0, maxChars);
            snippets.add(Map.of(
                    "text", text,
                    "title", (String) props.getOrDefault("item_title", "N/A"),
                    "type", (String) props.getOrDefault("type", "N/A")));
        }
        return snippets;
    }

    static String formatContext(List<Map<String, String>> snippets) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < snippets.size(); i++) {
            if (i > 0) sb.append("\n---\n");
            Map<String, String> s = snippets.get(i);
            sb.append(String.format("[Source %d: %s (%s)]\n%s\n", i+1, s.get("title"), s.get("type"), s.get("text")));
        }
        return sb.toString();
    }

    static String generateWithContext(String query, String context) throws Exception {
        String token = ensureValidToken();
        Map<String, Object> payload = Map.of(
                "messages", List.of(
                        Map.of("role", "system", "content",
                                "You are a helpful assistant. Answer the user's question based on the " +
                                "provided context. If the context doesn't contain relevant information, " +
                                "say so honestly."),
                        Map.of("role", "user", "content",
                                "Context:\n" + context + "\n\nQuestion: " + query)),
                "auto_routing", true, "max_tokens", 3000);
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(COMPLETIONS_URL))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
                .timeout(Duration.ofSeconds(60)).build();
        HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
        Map result = gson.fromJson(resp.body(), Map.class);
        List<Map> choices = (List<Map>) result.get("choices");
        Map msg = (Map) choices.get(0).get("message");
        return (String) msg.get("content");
    }

    public static void main(String[] args) throws Exception {
        String query = args.length > 0 ? args[0] : "How can I know my purpose?";
        int limit = args.length > 1 ? Integer.parseInt(args[1]) : 5;

        System.out.printf("RAG Search for: '%s'%n%n", query);

        System.out.println("Step 1: Searching for relevant content...");
        Map results = search(query, limit);
        List<Map> data = (List<Map>) results.get("data");
        System.out.printf("Found %d results%n%n", data != null ? data.size() : 0);

        System.out.println("Step 2: Extracting snippets...");
        var snippets = extractSnippets(results, 5, 500);
        String context = formatContext(snippets);
        System.out.printf("Extracted %d snippets%n%n", snippets.size());

        System.out.println("Step 3: Generating response with context...\n");
        String response = generateWithContext(query, context);

        System.out.println("=== Generated Response ===");
        System.out.println(response);
        System.out.println("\n=== Sources Used ===");
        for (var s : snippets) {
            System.out.printf("- %s (%s)%n", s.get("title"), s.get("type"));
        }
    }
}

What You’ll See

The RAG flow searches, extracts context, then generates an AI response grounded in your content:
RAG Search for: 'How can I know my purpose?'

Step 1: Searching for relevant content...
Found 5 results

Step 2: Extracting snippets...
Extracted 5 snippets

Step 3: Generating response with context...

=== Generated Response ===
Based on the provided articles, finding purpose involves orienting your life
toward God and cultivating specific spiritual qualities. Here are a few key ideas:

- **Relationship with God:** True happiness and purpose are found in your
  relationship with God and aligning your life with His will. (Sources 3, 5)

- **The Beatitudes:** Jesus provided a "blueprint for blessedness" in the
  Beatitudes (Matthew 5:3-12). (Sources 1, 5)

=== Sources Used ===
- Finding True Happiness (Article)
- Finding True Happiness (Article)
- Beatitudes True Happiness (Article)

Run the Cookbook Example

python search_advanced.py rag "How can I know my purpose?" 5
node search-advanced.js rag "How can I know my purpose?" 5
npx ts-node search-advanced.ts rag "How can I know my purpose?" 5
php search_advanced.php rag "How can I know my purpose?" 5
go run . rag "How can I know my purpose?" 5
mvn exec:java -Dexec.args='rag "How can I know my purpose?" 5'

Key Concepts

  • auto_routing: true — Lets Gloo AI automatically select the best model
  • System prompt — Customize to match your use case (tone, format, domain rules)
  • Context formatting — Source labels help the LLM cite correctly
  • Token budget — Keep context concise. 3-5 snippets of ~500 chars each works well

Search + Completions V2 vs Grounded Completions

Search + Completions V2Grounded Completions
ControlFull control over context, prompts, orderingGloo handles context automatically
ComplexityMore code, more flexibilitySingle API call
Custom promptsYes — any system promptLimited customization
Context formattingYou control structure and orderingGloo optimizes automatically
Best forCustom UX, domain-specific needsQuick prototyping, standard Q&A
Both approaches use the same underlying search. Start with Grounded Completions if you want simplicity, then switch to Search + Completions V2 when you need more control.

Try It: Frontend Example

The cookbook includes a browser-based frontend that connects to a proxy server, giving you a visual way to test both search and RAG. The proxy server keeps your credentials secure on the server side.

Architecture

Browser (HTML/JS) → Proxy Server (localhost:3000) → Gloo AI APIs
The proxy server exposes two endpoints:
  • GET /api/search?q=<query>&limit=<limit> — Basic search
  • POST /api/search/rag — Search + RAG with Completions V2

Start the Proxy Server

Each language includes a proxy server. Start one:
cd search-tutorial/python
source venv/bin/activate
python server.py
cd search-tutorial/javascript
node server.js
cd search-tutorial/typescript
npx ts-node server.ts
cd search-tutorial/php
php -S localhost:3000 server.php
cd search-tutorial/go
go run . server
cd search-tutorial/java
mvn exec:java -Dexec.args="server"
Then open http://localhost:3000 in your browser.

Search Results

Enter a query and click Search to see results with titles, content types, and relevance scores:
Search results showing content cards with titles, types, and relevance percentages

AI-Powered Answers (RAG)

Click Ask AI to send the same query through the RAG pipeline. The AI generates a response grounded in your search results, with sources listed:
AI response generated from search results with source citations
The frontend is language-agnostic — the same HTML/JS works with any language’s proxy server. This is one approach; customize for your branding and framework.

Complete Working Examples

View Complete Code

Clone or browse the complete working examples for all 6 languages (JavaScript, TypeScript, Python, PHP, Go, Java) with setup instructions, proxy servers, and a browser-based frontend.

What’s Included

Each language implementation provides:
  • auth — Shared OAuth2 token management with automatic refresh
  • config — Centralized configuration (URLs, env vars, RAG settings)
  • search_basic — Basic search (CLI script)
  • search_advanced — Advanced search + RAG helpers (CLI script)
  • server — Proxy server exposing REST endpoints for the frontend

Troubleshooting

No Results Returned

  • Missing certainty: Add "certainty": 0.5 to your payload. The API defaults to 0.75 when omitted, which may be too strict.
  • Content not indexed: Test in the Search Playground first. If results appear there but not via API, check your tenant name.
  • Wrong tenant: Results are scoped to your publisher. Verify the tenant name matches your publisher in Organizations.

403 Forbidden

  • You can only access your own publisher’s content. Other tenant names return 403.
  • Verify your Client ID and Client Secret are correct and not expired.

Slow Responses

  • Reduce the limit parameter. Request time rises non-linearly with larger result sets.
  • Start with limit=10 and increase only as needed.

Authentication Errors

  • Tokens expire. Implement token refresh logic (see Authentication Tutorial).
  • Ensure you’re using Bearer {token} in the Authorization header.

Empty RAG Responses

  • Verify search returns results before calling Completions V2.
  • Use auto_routing: true instead of specifying a model name.

Next Steps

Grounded Completions

Simpler RAG approach — single API call with automatic context management.

Upload Files

Add more content to your Data Engine for richer search results.

Completions V2 API

Full control over LLM interactions with custom prompts and parameters.

Search API Reference

Complete endpoint documentation with request/response schemas.