Skip to Content

Search

GenSX’s Cloud search service provides full-text and vector search for AI applications. It enables you to store, query, and manage vector embeddings for semantic search, retrieval-augmented generation (RAG), and other AI use cases.

Search is powered by turbopuffer, fully featured and ready for AI workloads:

  • Combined vector and keyword search: Perform hybrid searches using both semantic similarity (vectors) and keyword matching (BM25).
  • Millisecond query latency: Get results quickly, even with large vector collections.
  • Flexible filtering: Apply metadata filters to narrow down search results based on categories, timestamps, or any custom attributes.

Basic usage

To use search in your GenSX application:

  1. Install the storage package:

    npm install @gensx/storage
  2. Add the SearchProvider to your workflow:

    import { SearchProvider } from "@gensx/storage"; const Workflow = gensx.Component("SearchWorkflow", { input }) => ( <SearchProvider> <YourComponent input={input} /> </SearchProvider> );
  3. Access search namespaces within your components using the useSearch hook:

    import { useSearch } from "@gensx/storage"; const search = await useSearch("documents");

Storing vector embeddings

The first step in using search is to convert your data into vector embeddings and store them:

import * as gensx from "@gensx/core"; import { OpenAIEmbedding } from "@gensx/openai"; import { useSearch } from "@gensx/storage"; const IndexDocuments = gensx.Component( "IndexDocuments", async ({ documents }) => { // Get access to a search namespace const search = await useSearch("documents"); // Generate embeddings for the documents const embeddings = await OpenAIEmbedding.run({ model: "text-embedding-3-small", input: documents.map((doc) => doc.text), }); // Store the embeddings with original text as metadata await search.write({ upsertRows: documents.map((doc, index) => ({ id: doc.id, vector: embeddings.data[index].embedding, text: doc.text, category: doc.category, createdAt: new Date().toISOString(), })), distanceMetric: "cosine_distance", }); return { success: true, count: documents.length }; }, );

Searching for similar documents

Once you’ve stored embeddings, you can search for semantically similar content:

const SearchDocuments = gensx.Component( "SearchDocuments", async ({ query, category }) => { // Get access to the search namespace const search = await useSearch("documents"); // Generate an embedding for the query const embedding = await OpenAIEmbedding.run({ model: "text-embedding-3-small", input: query, }); // Build query options const queryOptions = { vector: embedding.data[0].embedding, includeAttributes: true, topK: 5, // Return top 5 results }; // Add filters if category is specified if (category) { queryOptions.filters = { where: { category: { $eq: category } }, }; } // Perform the search const results = await search.query(queryOptions); // Process and return results return results.map((result) => ({ id: result.id, text: result.attributes?.text, score: result.score, })); }, );

Building a RAG application

Retrieval-Augmented Generation (RAG) is one of the most common use cases for vector search. Here’s how to build a complete RAG workflow:

Step 1: Index your documents

First, create a component to prepare and index your documents:

const PrepareDocuments = gensx.Component("PrepareDocuments", async () => { // Sample baseball player data const documents = [ { id: "1", text: "Marcus Bennett is a first baseman for the Portland Pioneers. He has 32 home runs this season.", category: "player", }, { id: "2", text: "Ethan Carter plays shortstop for the San Antonio Stallions with 24 home runs.", category: "player", }, { id: "3", text: "The Portland Pioneers are leading the Western Division with a 92-70 record.", category: "team", }, ]; // Index the documents return <IndexDocuments documents={documents} />; });

Step 2: Create a query tool

Next, create a tool that can access your search index:

import { GSXTool } from "@gensx/openai"; import { z } from "zod"; // Define a tool to query the search index const queryTool = new GSXTool({ name: "query", description: "Query the baseball knowledge base", schema: z.object({ query: z.string().describe("The text query to search for"), }), run: async ({ query }) => { // Access search index const search = await useSearch("baseball"); // Generate query embedding const embedding = await OpenAIEmbedding.run({ model: "text-embedding-3-small", input: query, }); // Search for relevant documents const results = await search.query({ vector: embedding.data[0].embedding, includeAttributes: true, }); // Return formatted results return JSON.stringify( results.map((r) => r.attributes?.text), null, 2, ); }, });

Step 3: Create the RAG agent

Now, create an agent that uses the query tool to access relevant information:

const RagAgent = gensx.Component("RagAgent", ({ question }) => ( <GSXChatCompletion messages={[ { role: "system", content: `You are a baseball expert assistant. Use the query tool to look up relevant information before answering questions.`, }, { role: "user", content: question }, ]} model="gpt-4o-mini" tools={[queryTool]} > {(result) => result.choices[0].message.content} </GSXChatCompletion> ));

Step 4: Combine Everything in a Workflow

Finally, put it all together in a workflow:

const RagWorkflow = gensx.Component( "RagWorkflow", async ({ question, shouldReindex }) => { // Optionally reindex documents if (shouldReindex) { await PrepareDocuments(); } // Use the RAG agent to answer the question return <RagAgent question={question} />; }, );

Practical examples

Agent memory system

One powerful application of vector search is creating a long-term memory system for AI agents:

import * as gensx from "@gensx/core"; import { OpenAIEmbedding } from "@gensx/openai"; import { useSearch } from "@gensx/storage"; // Component to store a memory const StoreMemory = gensx.Component( "StoreMemory", async ({ userId, memory, importance = "medium" }) => { const search = await useSearch(`memories-${userId}`); // Generate embedding for this memory const embedding = await OpenAIEmbedding.run({ model: "text-embedding-3-small", input: memory, }); // Store the memory with metadata await search.write({ upsertRows: [ { id: `memory-${Date.now()}`, vector: embedding.data[0].embedding, content: memory, timestamp: new Date().toISOString(), importance: importance, // "high", "medium", "low" source: "user-interaction", }, ], distanceMetric: "cosine_distance", }); return { success: true }; }, ); // Component to recall relevant memories const RecallMemories = gensx.Component( "RecallMemories", async ({ userId, context, maxResults = 5 }) => { const search = await useSearch(`memories-${userId}`); // Generate embedding for the context const embedding = await OpenAIEmbedding.run({ model: "text-embedding-3-small", input: context, }); // Query for relevant memories, prioritizing important ones const results = await search.query({ vector: embedding.data[0].embedding, topK: maxResults, includeAttributes: true, // Optional: rank by both relevance and importance rankBy: ["attributes.importance", "asc"], }); // Format memories for the agent return results.map((result) => ({ content: result.attributes?.content, timestamp: result.attributes?.timestamp, relevance: result.score.toFixed(3), })); }, ); // Component that uses memories in a conversation const MemoryAwareAgent = gensx.Component( "MemoryAwareAgent", async ({ userId, userMessage }) => { // Recall relevant memories based on the current conversation const memories = await RecallMemories({ userId, context: userMessage, maxResults: 3, }); // Use memories to inform the response const response = await ChatCompletion.run({ model: "gpt-4o-mini", messages: [ { role: "system", content: `You are an assistant with memory. Consider these relevant memories about this user: ${memories.map((m) => `[${m.timestamp}] ${m.content} (relevance: ${m.relevance})`).join("\n")}`, }, { role: "user", content: userMessage }, ], }); // Store this interaction as a new memory await StoreMemory({ userId, memory: `User asked: "${userMessage}". I replied: "${response}"`, importance: "medium", }); return response; }, );

Another powerful application is a knowledge base with faceted search capabilities:

const SearchKnowledgeBase = gensx.Component( "SearchKnowledgeBase", async ({ query, filters = {} }) => { const search = await useSearch("knowledge-base"); // Generate embedding for the query const embedding = await OpenAIEmbedding.run({ model: "text-embedding-3-small", input: query, }); // Build filter conditions from user-provided filters let filterConditions = ["And", []]; if (filters.category) { filterConditions[1].push(["category", "Eq", filters.category]); } if (filters.dateRange) { filterConditions[1].push(["publishedDate", "Gte", filters.dateRange.start]); filterConditions[1].push(["publishedDate", "Lte", filters.dateRange.end]); } if (filters.tags && filters.tags.length > 0) { filterConditions[1].push(["tags", "ContainsAny", filters.tags]); } // Perform hybrid search (vector + keyword) with filters const results = await search.query({ vector: embedding.data[0].embedding, rankBy: ["text", "BM25", query], includeAttributes: true, topK: 10, filters: filterConditions[1].length > 0 ? filterConditions : undefined, }); // Return formatted results return results.map((result) => ({ title: result.attributes?.title, snippet: result.attributes?.snippet, url: result.attributes?.url, category: result.attributes?.category, tags: result.attributes?.tags, score: result.score, })); }, );

Advanced usage

Filtering by metadata

Use filters to narrow down search results:

const search = await useSearch("articles"); // Search with filters const results = await search.query({ vector: queryEmbedding, filters: [ "And", [ ["category", "Eq", "sports"], ["publishDate", "Gte", "2023-01-01"], ["publishDate", "Lt", "2024-01-01"], ["author", "In", ["Alice", "Bob", "Carol"]], ], ], });

Updating schema

Manage your vector collection’s schema:

const search = await useSearch("products"); // Get current schema const currentSchema = await search.getSchema(); // Update schema to add new fields await search.updateSchema({ ...currentSchema, newField: { type: "number" }, anotherField: { type: "string[]" }, });

Reference

See the search component reference for full details.

Last updated on