> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gloo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Building Custom Search with the Search API

> Learn how to build your own search experience using the Gloo AI Search API.

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.

<Info>
  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.
</Info>

## Prerequisites

Before starting, ensure you have:

* A Gloo AI Studio account
* Your Client ID and Client Secret from the [API Credentials page](/studio/manage-api-credentials)
* Your Tenant (publisher) name from [Organizations](/studio/manage-organizations) in Studio
* Content uploaded to the Data Engine (see [Upload Files Tutorial](/tutorials/upload-files))
* **Authentication setup** — Complete the [Authentication Tutorial](/tutorials/authentication) first

<Card title="Working Code Sample" icon="github" href="https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/search-tutorial">
  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.
</Card>

<Tip>
  The code snippets in this tutorial are **simplified and self-contained** —
  designed for readability and easy copy-paste. The [cookbook
  examples](https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/search-tutorial)
  use a modular architecture plus production niceties. Both implement the same
  APIs and patterns.
</Tip>

***

## 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

| Parameter    | Description                                      |
| ------------ | ------------------------------------------------ |
| `query`      | The search query string                          |
| `collection` | Always `"GlooProd"`                              |
| `tenant`     | Your publisher (tenant) name                     |
| `limit`      | Number of results to return (10-100 recommended) |

### Optional Parameters

| Parameter   | Type        | Description                                                                                                                                                  |
| ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `certainty` | float (0-1) | Minimum relevance threshold. The [Search Playground](/studio/playground-search) defaults to `0.5`. We recommend starting with `0.5` and adjusting as needed. |

<Warning>
  **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.
</Warning>

### Response Structure

Each result in the `data` array contains:

```json theme={null}
{
  "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](/studio/playground-search) to verify your content is indexed and understand the response structure.

1. Navigate to **Playground** in [Gloo AI Studio](https://studio.ai.gloo.com)
2. Select the **Search** tab
3. Choose your publisher from the dropdown
4. Enter a query and review the results

<Tip>
  **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.
</Tip>

***

## Step 1: Basic Search

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.

<CodeGroup>
  ```python Python theme={null}
  #!/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()
  ```

  ```javascript JavaScript theme={null}
  /**
   * 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();
    });
  })();
  ```

  ```typescript TypeScript theme={null}
  /**
   * 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 PHP theme={null}
  <?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";
      }
  }
  ```

  ```go Go theme={null}
  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()
      }
  }
  ```

  ```java Java theme={null}
  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();
          }
      }
  }
  ```
</CodeGroup>

### 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:

<CodeGroup>
  ```bash Python theme={null}
  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
  ```

  ```bash JavaScript theme={null}
  cd search-tutorial/javascript
  npm install
  node search-basic.js "How can I know my purpose?" 5
  ```

  ```bash TypeScript theme={null}
  cd search-tutorial/typescript
  npm install
  npx ts-node search-basic.ts "How can I know my purpose?" 5
  ```

  ```bash PHP theme={null}
  cd search-tutorial/php
  composer install
  php search_basic.php "How can I know my purpose?" 5
  ```

  ```bash Go theme={null}
  cd search-tutorial/go
  go mod download
  go run . search "How can I know my purpose?" 5
  ```

  ```bash Java theme={null}
  cd search-tutorial/java
  mvn clean install -q
  mvn exec:java -Dexec.args='search "How can I know my purpose?" 5'
  ```
</CodeGroup>

***

## 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.

<Info>
  **Two approaches to RAG with Gloo AI:**

  * **Search API + Completions V2** (this section): Full control over context, prompts, and formatting
  * **[Grounded Completions](/tutorials/completions-grounded)**: Single API call, simpler but less control

  Both retrieve identical content. The difference is who controls how that content is presented to the LLM.
</Info>

### 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

<CodeGroup>
  ```python Python theme={null}
  #!/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']})")
  ```

  ```javascript JavaScript theme={null}
  /**
   * 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})`));
  })();
  ```

  ```typescript TypeScript theme={null}
  /**
   * 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 PHP theme={null}
  <?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";
  }
  ```

  ```go Go theme={null}
  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) }
  }
  ```

  ```java Java theme={null}
  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"));
          }
      }
  }
  ```
</CodeGroup>

### 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

<CodeGroup>
  ```bash Python theme={null}
  python search_advanced.py rag "How can I know my purpose?" 5
  ```

  ```bash JavaScript theme={null}
  node search-advanced.js rag "How can I know my purpose?" 5
  ```

  ```bash TypeScript theme={null}
  npx ts-node search-advanced.ts rag "How can I know my purpose?" 5
  ```

  ```bash PHP theme={null}
  php search_advanced.php rag "How can I know my purpose?" 5
  ```

  ```bash Go theme={null}
  go run . rag "How can I know my purpose?" 5
  ```

  ```bash Java theme={null}
  mvn exec:java -Dexec.args='rag "How can I know my purpose?" 5'
  ```
</CodeGroup>

### 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 V2                      | Grounded Completions               |
| ---------------------- | -------------------------------------------- | ---------------------------------- |
| **Control**            | Full control over context, prompts, ordering | Gloo handles context automatically |
| **Complexity**         | More code, more flexibility                  | Single API call                    |
| **Custom prompts**     | Yes — any system prompt                      | Limited customization              |
| **Context formatting** | You control structure and ordering           | Gloo optimizes automatically       |
| **Best for**           | Custom UX, domain-specific needs             | Quick prototyping, standard Q\&A   |

<Tip>
  Both approaches use the same underlying search. Start with [Grounded
  Completions](/tutorials/completions-grounded) if you want simplicity, then
  switch to Search + Completions V2 when you need more control.
</Tip>

***

## 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:

<CodeGroup>
  ```bash Python theme={null}
  cd search-tutorial/python
  source venv/bin/activate
  python server.py
  ```

  ```bash JavaScript theme={null}
  cd search-tutorial/javascript
  node server.js
  ```

  ```bash TypeScript theme={null}
  cd search-tutorial/typescript
  npx ts-node server.ts
  ```

  ```bash PHP theme={null}
  cd search-tutorial/php
  php -S localhost:3000 server.php
  ```

  ```bash Go theme={null}
  cd search-tutorial/go
  go run . server
  ```

  ```bash Java theme={null}
  cd search-tutorial/java
  mvn exec:java -Dexec.args="server"
  ```
</CodeGroup>

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:

<Frame>
  <img src="https://mintcdn.com/gloo-b243725a/LtIg_4O_FVvzu3BT/images/search-tutorial_search_results.png?fit=max&auto=format&n=LtIg_4O_FVvzu3BT&q=85&s=173c13f7f7a2c1b00c5edbf093ebd452" alt="Search results showing content cards with titles, types, and relevance percentages" width="807" height="692" data-path="images/search-tutorial_search_results.png" />
</Frame>

### 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:

<Frame>
  <img src="https://mintcdn.com/gloo-b243725a/LtIg_4O_FVvzu3BT/images/search-tutorial_search_plus_rag_results.png?fit=max&auto=format&n=LtIg_4O_FVvzu3BT&q=85&s=541975f3d1badd40b86e3656c4e1c458" alt="AI response generated from search results with source citations" width="903" height="863" data-path="images/search-tutorial_search_plus_rag_results.png" />
</Frame>

<Info>
  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.
</Info>

***

## Complete Working Examples

<Card title="View Complete Code" icon="github" href="https://github.com/GlooDeveloper/gloo-ai-docs-cookbook/tree/main/search-tutorial">
  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.
</Card>

### 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](/studio/playground-search) 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](/studio/manage-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](/tutorials/authentication)).
* 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

<CardGroup cols={2}>
  <Card title="Grounded Completions" href="/tutorials/completions-grounded" icon="anchor">
    Simpler RAG approach — single API call with automatic context management.
  </Card>

  <Card title="Upload Files" href="/tutorials/upload-files" icon="upload">
    Add more content to your Data Engine for richer search results.
  </Card>

  <Card title="Completions V2 API" href="/api-guides/completions-v2" icon="keyboard">
    Full control over LLM interactions with custom prompts and parameters.
  </Card>

  <Card title="Search API Reference" href="/api-reference/data/search-publisher-data" icon="book">
    Complete endpoint documentation with request/response schemas.
  </Card>
</CardGroup>
