Building AI Product Features in 2026: Embeddings, Semantic Search UX, and Content Generation
Build AI features users actually love: embeddings-based similarity and recommendations, semantic search with ranking UI, AI content generation with streaming, and evaluation frameworks.
Building AI Product Features in 2026: Embeddings, Semantic Search UX, and Content Generation
Most AI features added to products in 2025โ2026 fail not because the technology is wrong, but because the product design is wrong. A chatbot bolted onto an existing product confuses users. Semantic search that returns semantically-similar-but-wrong results damages trust. AI-generated content that's generic and uneditable frustrates writers.
This post covers the implementation patterns for AI features that actually improve the product: embeddings-based similarity and recommendations, semantic search with a trust-building UX, and LLM content generation with streaming and user control.
Embeddings: The Foundation
Embeddings convert text (or any content) into a high-dimensional vector. Items with similar meaning have vectors that are close together in this space. This enables: "find items similar to this," "recommend based on history," and "search by meaning, not exact words."
// src/lib/embeddings.ts
import OpenAI from 'openai';
import { db } from '@/lib/db';
const openai = new OpenAI();
// Batch embedding generation (more efficient than one-at-a-time)
export async function generateEmbeddings(
texts: string[],
model = 'text-embedding-3-small', // 1536 dimensions, cheap and fast
): Promise<number[][]> {
if (texts.length === 0) return [];
// OpenAI accepts up to 2048 inputs per request
const chunks: string[][] = [];
for (let i = 0; i < texts.length; i += 100) {
chunks.push(texts.slice(i, i + 100));
}
const embeddings: number[][] = [];
for (const chunk of chunks) {
const response = await openai.embeddings.create({ model, input: chunk });
embeddings.push(...response.data.map((d) => d.embedding));
}
return embeddings;
}
// Store content embedding in PostgreSQL with pgvector
export async function indexContent(content: {
id: string;
text: string; // The text to embed
metadata: Record<string, unknown>;
}): Promise<void> {
const [embedding] = await generateEmbeddings([content.text]);
await db.query(
`INSERT INTO content_embeddings (content_id, embedding, metadata, indexed_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (content_id) DO UPDATE
SET embedding = EXCLUDED.embedding,
metadata = EXCLUDED.metadata,
indexed_at = now()`,
[content.id, JSON.stringify(embedding), JSON.stringify(content.metadata)],
);
}
Recommendations: "Similar to This"
// src/features/recommendations/similar-items.ts
export async function findSimilarItems(
contentId: string,
options: {
limit?: number;
minSimilarity?: number; // 0โ1; higher = more similar
filterCategory?: string;
} = {},
): Promise<SimilarItem[]> {
const { limit = 10, minSimilarity = 0.7, filterCategory } = options;
// Get the source item's embedding
const { rows: [source] } = await db.query<{ embedding: string }>(
'SELECT embedding FROM content_embeddings WHERE content_id = $1',
[contentId],
);
if (!source) return [];
// Find similar items by cosine similarity
const { rows } = await db.query<SimilarItem>(
`SELECT
ce.content_id,
c.title,
c.category,
c.thumbnail_url,
1 - (ce.embedding <=> $1::vector) AS similarity
FROM content_embeddings ce
JOIN content c ON c.id = ce.content_id
WHERE ce.content_id != $2 -- Exclude source
AND ($3::text IS NULL OR c.category = $3) -- Optional filter
AND 1 - (ce.embedding <=> $1::vector) >= $4 -- Min similarity threshold
ORDER BY ce.embedding <=> $1::vector -- Closest first
LIMIT $5`,
[source.embedding, contentId, filterCategory ?? null, minSimilarity, limit],
);
return rows;
}
// Personalized recommendations based on user history
export async function getPersonalizedRecommendations(
userId: string,
limit = 10,
): Promise<SimilarItem[]> {
// Get embeddings of items the user recently engaged with
const { rows: history } = await db.query<{ embedding: string }>(
`SELECT ce.embedding
FROM user_interactions ui
JOIN content_embeddings ce ON ce.content_id = ui.content_id
WHERE ui.user_id = $1 AND ui.interaction_type = 'view'
ORDER BY ui.created_at DESC
LIMIT 10`,
[userId],
);
if (history.length === 0) return getTrendingContent(limit);
// Average the embeddings to create a user taste profile
// (Simple but effective: centroid of recent interest vectors)
const embeddingDim = 1536;
const avg = new Array(embeddingDim).fill(0);
for (const { embedding } of history) {
const vec = JSON.parse(embedding) as number[];
vec.forEach((v, i) => { avg[i] += v / history.length; });
}
// Find content closest to the user's average taste vector
const { rows } = await db.query<SimilarItem>(
`SELECT
ce.content_id,
c.title,
c.category,
1 - (ce.embedding <=> $1::vector) AS similarity
FROM content_embeddings ce
JOIN content c ON c.id = ce.content_id
WHERE ce.content_id NOT IN (
SELECT content_id FROM user_interactions
WHERE user_id = $2 AND interaction_type = 'view'
)
ORDER BY ce.embedding <=> $1::vector
LIMIT $3`,
[JSON.stringify(avg), userId, limit],
);
return rows;
}
๐ค AI Is Not the Future โ It Is Right Now
Businesses using AI automation cut manual work by 60โ80%. We build production-ready AI systems โ RAG pipelines, LLM integrations, custom ML models, and AI agent workflows.
- LLM integration (OpenAI, Anthropic, Gemini, local models)
- RAG systems that answer from your own data
- AI agents that take real actions โ not just chat
- Custom ML models for prediction, classification, detection
Semantic Search UX: Building Trust
Semantic search fails in UX when users can't understand why a result was returned. The trust-building pattern: always explain relevance and provide fallback to keyword search.
// src/features/search/SemanticSearchResults.tsx
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
interface SearchResult {
id: string;
title: string;
excerpt: string;
score: number; // 0โ1 similarity score
matchType: 'semantic' | 'keyword' | 'hybrid';
highlightedTerms?: string[]; // For keyword component of match
}
export function SemanticSearchResults({ query }: { query: string }) {
const [showExplanations, setShowExplanations] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ['search', query],
queryFn: () => fetch(`/api/search?q=${encodeURIComponent(query)}`).then((r) => r.json()),
enabled: query.length > 2,
});
if (isLoading) return <SearchSkeleton />;
return (
<div>
{/* Search mode indicator + trust signal */}
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-500">
{data?.total} results for “{query}”
{data?.semanticEnabled && ' ยท including conceptually related content'}
</p>
<button
onClick={() => setShowExplanations(!showExplanations)}
className="text-sm text-blue-600 hover:underline"
>
{showExplanations ? 'Hide' : 'Why these results?'}
</button>
</div>
{/* Results */}
{data?.results.map((result: SearchResult) => (
<SearchResultCard
key={result.id}
result={result}
showExplanation={showExplanations}
query={query}
/>
))}
{/* Fallback: didn't find what you expected? */}
{data?.results.length > 0 && (
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">
Not finding what you expected?{' '}
<a href={`/search?q=${encodeURIComponent(query)}&mode=exact`}
className="text-blue-600 hover:underline">
Search for exact phrase
</a>
</p>
</div>
)}
</div>
);
}
function SearchResultCard({
result,
showExplanation,
query,
}: {
result: SearchResult;
showExplanation: boolean;
query: string;
}) {
return (
<div className="border rounded-lg p-4 mb-3 hover:border-blue-200 transition-colors">
<h3 className="font-semibold text-gray-900">{result.title}</h3>
<p className="text-sm text-gray-600 mt-1">{result.excerpt}</p>
{/* Match type badge */}
<div className="flex items-center gap-2 mt-2">
<span className={`text-xs px-2 py-0.5 rounded-full ${
result.matchType === 'keyword' ? 'bg-green-100 text-green-700' :
result.matchType === 'semantic' ? 'bg-purple-100 text-purple-700' :
'bg-blue-100 text-blue-700'
}`}>
{result.matchType === 'keyword' ? 'Exact match' :
result.matchType === 'semantic' ? 'Related topic' :
'Best match'}
</span>
{/* Relevance explanation (when requested) */}
{showExplanation && result.matchType === 'semantic' && (
<span className="text-xs text-gray-500">
Conceptually similar to “{query}”
({Math.round(result.score * 100)}% relevance)
</span>
)}
</div>
</div>
);
}
AI Content Generation with Streaming
// src/features/writing/AISuggestionPanel.tsx
'use client';
import { useState, useCallback } from 'react';
interface GeneratedContent {
text: string;
isComplete: boolean;
}
export function AISuggestionPanel({
existingContent,
onAccept,
}: {
existingContent: string;
onAccept: (text: string) => void;
}) {
const [generated, setGenerated] = useState<GeneratedContent | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [prompt, setPrompt] = useState('');
const generateContent = useCallback(async () => {
if (!prompt.trim()) return;
setIsGenerating(true);
setGenerated({ text: '', isComplete: false });
try {
const response = await fetch('/api/ai/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, existingContent }),
});
if (!response.body) throw new Error('No response body');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE format: "data: <text>\n\n"
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const text = line.slice(6);
if (text === '[DONE]') {
setGenerated((prev) => prev ? { ...prev, isComplete: true } : null);
} else {
setGenerated((prev) => prev
? { ...prev, text: prev.text + text }
: { text, isComplete: false }
);
}
}
}
}
} catch (err) {
console.error('Generation failed:', err);
setGenerated(null);
} finally {
setIsGenerating(false);
}
}, [prompt, existingContent]);
return (
<div className="border rounded-lg p-4 bg-purple-50">
<h3 className="font-medium text-purple-900 mb-3">โจ AI Writing Assistant</h3>
<div className="flex gap-2 mb-4">
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && generateContent()}
placeholder='e.g., "Make this more concise" or "Add a call to action"'
className="flex-1 text-sm border rounded px-3 py-2"
disabled={isGenerating}
/>
<button
onClick={generateContent}
disabled={isGenerating || !prompt.trim()}
className="btn-primary text-sm"
>
{isGenerating ? 'Generating...' : 'Generate'}
</button>
</div>
{generated && (
<div className="bg-white border rounded p-3 mb-3">
<p className="text-sm text-gray-800 whitespace-pre-wrap">
{generated.text}
{!generated.isComplete && (
<span className="inline-block w-1 h-4 bg-purple-500 ml-0.5 animate-pulse" />
)}
</p>
</div>
)}
{generated?.isComplete && (
<div className="flex gap-2">
<button
onClick={() => onAccept(generated.text)}
className="btn-primary text-sm"
>
Use this
</button>
<button
onClick={() => setGenerated(null)}
className="btn-secondary text-sm"
>
Discard
</button>
<button
onClick={generateContent}
className="text-sm text-purple-600 hover:underline"
>
Try again
</button>
</div>
)}
</div>
);
}
Streaming API Route
// src/api/ai/generate.ts (Next.js App Router)
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
export async function POST(req: Request): Promise<Response> {
const { prompt, existingContent } = await req.json();
const stream = anthropic.messages.stream({
model: 'claude-haiku-4-5', // Fast model for UI streaming
max_tokens: 1024,
messages: [
{
role: 'user',
content: `You are a writing assistant. Here is the existing content:
<content>
${existingContent}
</content>
Task: ${prompt}
Provide only the improved/new text. No explanations or preamble.`,
},
],
});
// Return SSE stream
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (
chunk.type === 'content_block_delta' &&
chunk.delta.type === 'text_delta'
) {
controller.enqueue(
encoder.encode(`data: ${chunk.delta.text}\n\n`),
);
}
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
โก Your Competitors Are Already Using AI โ Are You?
We build AI systems that actually work in production โ not demos that die in a Colab notebook. From data pipeline to deployed model to real business outcomes.
- AI agent systems that run autonomously โ not just chatbots
- Integrates with your existing tools (CRM, ERP, Slack, etc.)
- Explainable outputs โ know why the model decided what it did
- Free AI opportunity audit for your business
Evaluation: Knowing If Your AI Feature Works
// src/__tests__/ai/search-quality.test.ts
// Offline evaluation: does semantic search return expected results?
describe('Semantic search quality', () => {
const testCases = [
{
query: 'how to reset my password',
expectedTopSlug: 'password-reset-guide',
description: 'Direct intent match',
},
{
query: 'cant login to my account',
expectedTopSlug: 'login-troubleshooting',
description: 'Typo + synonym match',
},
{
query: 'billing credit card update',
expectedTopSlug: 'update-payment-method',
description: 'Semantic near-match',
},
];
for (const { query, expectedTopSlug, description } of testCases) {
it(`finds "${expectedTopSlug}" for query: "${query}" (${description})`, async () => {
const results = await searchContent(query, { limit: 5 });
const topResult = results[0];
expect(topResult.slug).toBe(expectedTopSlug);
});
}
});
Working With Viprasol
We build AI product features that improve user experience โ from embeddings infrastructure through semantic search UX, recommendations, and streaming content generation.
What we deliver:
- pgvector embeddings pipeline for content indexing and similarity search
- Personalized recommendation engine based on user interaction history
- Semantic search implementation with trust-building UX patterns
- LLM content generation with streaming and user editing controls
- Offline evaluation framework for AI feature quality testing
โ Discuss your AI product features โ AI and machine learning services
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Want to Implement AI in Your Business?
From chatbots to predictive models โ harness the power of AI with a team that delivers.
Free consultation โข No commitment โข Response within 24 hours
Ready to automate your business with AI agents?
We build custom multi-agent AI systems that handle sales, support, ops, and content โ across Telegram, WhatsApp, Slack, and 20+ other platforms. We run our own business on these systems.