<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mixpeek]]></title><description><![CDATA[Mixpeek]]></description><link>https://mixpeekai.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6add695ce53654910ecdb991/e08391e8-51fa-4602-a7fe-bb1134a09049.png</url><title>Mixpeek</title><link>https://mixpeekai.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 15:06:26 GMT</lastBuildDate><atom:link href="https://mixpeekai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Multimodal Deep Research Agent]]></title><description><![CDATA[How we went from "please find me information about this image" to building an AI that can reason...]]></description><link>https://mixpeekai.hashnode.dev/building-a-multimodal-deep-research-agent</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/building-a-multimodal-deep-research-agent</guid><category><![CDATA[multimodal]]></category><category><![CDATA[#multimodalai]]></category><category><![CDATA[multimodalsearch]]></category><category><![CDATA[VectorDatabase]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:42:00 GMT</pubDate><content:encoded><![CDATA[<p><strong>How we went from "please find me information about this image" to building an AI that can reason across documents, videos, images, and audio like a digital Sherlock Holmes</strong></p>
<hr />
<p><strong>When Search Gets Visual</strong></p>
<p>Picture this: You're a market researcher trying to understand competitor product positioning. You have their marketing videos, product screenshots, PDFs of their whitepapers, and audio from their earnings calls. Traditional search? Good luck piecing that together manually. Enter multimodal deep research, where your AI agent doesn't just read the web, it <strong>sees</strong>, <strong>hears</strong>, and <strong>understands</strong> across every media type.</p>
<p>While the world obsessed over text-based DeepSearch in early 2025, we've been quietly building something more ambitious: <strong>multimodal deep research agents</strong> that can analyze visual content, extract insights from audio, process video frames, and synthesize findings across media types. Think of it as giving your research assistant eyes, ears, and the reasoning power to connect dots across formats.</p>
<hr />
<h2>Why Multimodal Deep Research Matters Now</h2>
<p>The shift from single-pass RAG to iterative DeepSearch was just the appetizer. The real game-changer? <strong>Cross-modal reasoning</strong>.</p>
<p>Consider these scenarios where text-only search fails spectacularly:</p>
<ul>
<li><strong>Product Intelligence</strong>: Analyzing competitor UIs from screenshots while cross-referencing their technical documentation</li>
<li><strong>Content Compliance</strong>: Scanning video content for brand safety while analyzing accompanying transcripts</li>
<li><strong>Market Research</strong>: Processing social media images, video reviews, and written feedback to understand sentiment trends</li>
<li><strong>Technical Documentation</strong>: Understanding architectural diagrams alongside code repositories and documentation</li>
</ul>
<p><strong>The Technical Reality Check</strong>: Most "multimodal" solutions today are just text search with image OCR bolted on. True multimodal deep research requires:</p>
<ol>
<li><strong>Native multimodal understanding</strong> (not just OCR + text search)</li>
<li><strong>Cross-modal reasoning</strong> (connecting insights across media types)</li>
<li><strong>Iterative refinement</strong> (the DeepSearch loop applied to multimedia)</li>
<li><strong>Context preservation</strong> across modalities</li>
</ol>
<hr />
<h2>Architecture Deep Dive: Building the Multimodal Engine</h2>
<h3>The Core Loop: Search → See → Hear → Reason</h3>
<p>Building on the text-based DeepSearch pattern, our multimodal agent follows an expanded loop:</p>
<pre><code class="language-javascript">// Multimodal DeepSearch Core Loop
while (tokenUsage &lt; budget &amp;&amp; attempts &lt;= maxAttempts) {
  const currentQuery = getNextQuery(gaps, originalQuestion);
  
  // Multimodal search across content types
  const searchResults = await multimodalSearch({
    text: await textSearch(currentQuery),
    images: await imageSearch(currentQuery), 
    videos: await videoSearch(currentQuery),
    audio: await audioSearch(currentQuery)
  });
  
  // Process results by modality
  const insights = await Promise.all([
    processTextContent(searchResults.text),
    processVisualContent(searchResults.images),
    processVideoContent(searchResults.videos),
    processAudioContent(searchResults.audio)
  ]);
  
  // Cross-modal reasoning
  const synthesis = await crossModalReasoning(insights, context);
  
  if (synthesis.isComplete) break;
  
  // Generate new gap questions based on multimodal analysis
  gaps.push(...synthesis.newQuestions);
}
</code></pre>
<h3>Modality-Specific Processing Pipelines</h3>
<p><strong>1. Visual Content Pipeline</strong></p>
<pre><code class="language-javascript">async function processVisualContent(images) {
  const results = [];
  
  for (const image of images) {
    // Multi-stage visual analysis
    const analysis = await Promise.all([
      // Scene understanding
      visionModel.analyzeScene(image.url),
      // Text extraction (OCR)
      extractTextFromImage(image.url),
      // Object detection
      detectObjects(image.url),
      // Facial recognition (if applicable)
      analyzeFaces(image.url)
    ]);
    
    // Combine visual insights
    const insight = await synthesizeVisualInsights(analysis, image.context);
    results.push(insight);
  }
  
  return results;
}
</code></pre>
<p><strong>2. Video Content Pipeline</strong></p>
<pre><code class="language-javascript">async function processVideoContent(videos) {
  const results = [];
  
  for (const video of videos) {
    // Extract keyframes for analysis
    const keyframes = await extractKeyframes(video.url, { interval: 30 });
    
    // Process audio track
    const audioAnalysis = await processAudioTrack(video.audioUrl);
    
    // Analyze visual progression
    const visualProgression = await analyzeFrameSequence(keyframes);
    
    // Combine temporal insights
    const temporalInsight = await synthesizeTemporalContent(
      visualProgression,
      audioAnalysis,
      video.metadata
    );
    
    results.push(temporalInsight);
  }
  
  return results;
}
</code></pre>
<p><strong>3. Audio Content Pipeline</strong></p>
<pre><code class="language-javascript">async function processAudioContent(audioFiles) {
  const results = [];
  
  for (const audio of audioFiles) {
    const analysis = await Promise.all([
      // Speech-to-text
      transcribeAudio(audio.url),
      // Speaker identification
      identifySpeakers(audio.url),
      // Sentiment analysis from voice
      analyzeToneAndSentiment(audio.url),
      // Background audio analysis
      analyzeAudioScene(audio.url)
    ]);
    
    const audioInsight = await synthesizeAudioInsights(analysis, audio.context);
    results.push(audioInsight);
  }
  
  return results;
}
</code></pre>
<hr />
<h2>The Cross-Modal Reasoning Engine</h2>
<p>Here's where the magic happens, and where most implementations fall flat. Cross-modal reasoning isn't just about processing different content types; it's about finding <strong>semantic connections</strong> across modalities.</p>
<h3>Implementation Strategy</h3>
<pre><code class="language-javascript">async function crossModalReasoning(insights, context) {
  // 1. Extract semantic embeddings for each insight
  const embeddings = await Promise.all(
    insights.map(insight =&gt; generateMultimodalEmbedding(insight))
  );
  
  // 2. Find cross-modal connections
  const connections = findSemanticConnections(embeddings, threshold=0.8);
  
  // 3. Build knowledge graph
  const knowledgeGraph = buildCrossModalGraph(insights, connections);
  
  // 4. Reason across the graph
  const reasoning = await reasonAcrossModalities(knowledgeGraph, context);
  
  return {
    synthesis: reasoning.conclusion,
    confidence: reasoning.confidence,
    newQuestions: reasoning.gaps,
    isComplete: reasoning.confidence &gt; 0.85
  };
}
</code></pre>
<p><strong>The Semantic Bridge Pattern</strong></p>
<p>One breakthrough we discovered: using <strong>semantic bridges</strong> to connect insights across modalities. For instance:</p>
<ul>
<li><strong>Visual-Text Bridge</strong>: Screenshot of a UI element + documentation describing that feature</li>
<li><strong>Audio-Visual Bridge</strong>: Speaker in video + voice sentiment analysis</li>
<li><strong>Temporal Bridge</strong>: Sequence of events across video frames + corresponding audio timeline</li>
</ul>
<hr />
<h2>Challenges I Faced Along the Way</h2>
<h3>Challenge 1: The Context Explosion Problem</h3>
<p><strong>The Problem</strong>: Multimodal content generates <strong>massive</strong> context. A single video can produce thousands of tokens from transcription, visual analysis, and metadata. Traditional context windows couldn't handle comprehensive multimodal analysis.</p>
<p><strong>The Solution</strong>: Hierarchical context compression with modality-aware summarization.</p>
<pre><code class="language-javascript">class ModalityAwareContextManager {
  constructor(maxTokens = 200000) {
    this.maxTokens = maxTokens;
    this.compressionRatios = {
      text: 0.3,      // Aggressive text compression
      visual: 0.5,    // Moderate visual compression  
      audio: 0.4,     // Audio needs more context
      temporal: 0.6   // Video sequences need flow preservation
    };
  }
  
  async compressContext(multimodalContext) {
    const compressed = {};
    
    for (const [modality, content] of Object.entries(multimodalContext)) {
      if (this.getTokenCount(content) &gt; this.getModalityLimit(modality)) {
        compressed[modality] = await this.smartCompress(content, modality);
      } else {
        compressed[modality] = content;
      }
    }
    
    return compressed;
  }
}
</code></pre>
<h3>Challenge 2: Multimodal Model Hallucinations</h3>
<p><strong>The Problem</strong>: Vision models confidently describing things that aren't there. Audio models inventing conversations. The reliability issues compound when you're reasoning across modalities.</p>
<p><strong>The Solution</strong>: Cross-modal validation and confidence scoring.</p>
<pre><code class="language-javascript">async function validateCrossModal(insight, supportingEvidence) {
  // Check consistency across modalities
  const consistencyScore = await checkModalityConsistency(
    insight,
    supportingEvidence
  );
  
  // Validate against external sources when possible
  const externalValidation = await validateAgainstKnownFacts(insight);
  
  // Confidence scoring
  const confidence = calculateConfidence(consistencyScore, externalValidation);
  
  return {
    insight,
    confidence,
    validated: confidence &gt; 0.7,
    warnings: insight.confidence &lt; 0.5 ? ["Low confidence insight"] : []
  };
}
</code></pre>
<h3>Challenge 3: The Modality Bias Problem</h3>
<p><strong>The Insight</strong>: Different modalities have different "authority" for different types of questions. Text is authoritative for facts, visuals for spatial relationships, audio for emotional context.</p>
<p><strong>The Solution</strong>: Modality-weighted reasoning with domain-specific authority.</p>
<hr />
<h2><strong>Practical Implementation Guide</strong></h2>
<h3><strong>Setting Up Your Multimodal Stack</strong></h3>
<pre><code class="language-javascript">// Core dependencies
import { OpenAI } from 'openai';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { AssemblyAI } from 'assemblyai';
import { Vision } from '@google-cloud/vision';
import { SpeechClient } from '@google-cloud/speech';

class MultimodalDeepResearchAgent {
  constructor(config) {
    this.textModel = new OpenAI({ apiKey: config.openaiKey });
    this.visionModel = new GoogleGenerativeAI(config.geminiKey);
    this.audioProcessor = new AssemblyAI({ apiKey: config.assemblyKey });
    this.objectDetector = new Vision.ImageAnnotatorClient();
    
    // Mixpeek integration for multimodal search
    this.multimodalSearch = new MixpeekClient({
      apiKey: config.mixpeekKey,
      enableCrossModal: true
    });
  }
  
  async research(query, options = {}) {
    const context = new MultimodalContext();
    const gaps = [query];
    let attempts = 0;
    
    while (gaps.length &gt; 0 &amp;&amp; attempts &lt; options.maxAttempts) {
      const currentQuery = gaps.shift();
      
      // Multimodal search
      const results = await this.multimodalSearch.search(currentQuery, {
        modalities: ['text', 'image', 'video', 'audio'],
        crossModal: true
      });
      
      // Process each modality
      const insights = await this.processAllModalities(results);
      
      // Cross-modal reasoning
      const reasoning = await this.reasonAcrossModalities(insights, context);
      
      if (reasoning.isComplete) {
        return this.generateReport(reasoning, context);
      }
      
      gaps.push(...reasoning.newQuestions);
      attempts++;
    }
    
    return this.generatePartialReport(context);
  }
}
</code></pre>
<h3><strong>Integration with Mixpeek for Multimodal Search</strong></h3>
<pre><code class="language-javascript">// Leveraging Mixpeek's multimodal capabilities
async function setupMixpeekIntegration() {
  const mixpeek = new MixpeekClient({
    apiKey: process.env.MIXPEEK_API_KEY,
    features: {
      crossModalSearch: true,
      semanticSimilarity: true,
      temporalAnalysis: true
    }
  });
  
  // Index your multimodal content
  await mixpeek.index({
    collection: 'research_corpus',
    sources: [
      { type: 'video', path: 's3://my-bucket/videos/' },
      { type: 'audio', path: 's3://my-bucket/audio/' },
      { type: 'image', path: 's3://my-bucket/images/' },
      { type: 'document', path: 's3://my-bucket/docs/' }
    ],
    processing: {
      extractFrames: true,
      transcribeAudio: true,
      ocrImages: true,
      semanticChunking: true
    }
  });
  
  return mixpeek;
}
</code></pre>
<hr />
<h2><strong>Performance Optimizations &amp; Trade-offs</strong></h2>
<h3><strong>The Parallel Processing Pipeline</strong></h3>
<p>Here's where we get into the juicy engineering details.</p>
<pre><code class="language-javascript">class OptimizedMultimodalProcessor {
  constructor() {
    this.processingPool = new WorkerPool(8); // Adjust based on your infra
    this.cacheLayer = new RedisCache();
    this.rateLimiter = new TokenBucket({
      capacity: 1000,
      refillRate: 100
    });
  }
  
  async processInParallel(multimodalResults) {
    // Smart batching based on processing requirements
    const batches = this.createOptimalBatches(multimodalResults);
    
    const results = await Promise.allSettled(
      batches.map(batch =&gt; this.processBatch(batch))
    );
    
    return this.mergeResults(results);
  }
  
  createOptimalBatches(results) {
    // Group by processing complexity and API rate limits
    const textBatch = results.text.slice(0, 20); // OpenAI batch limit
    const imageBatches = this.chunkArray(results.images, 5); // Vision API limits
    const videoBatches = results.videos.map(v =&gt; [v]); // Process individually
    
    return { textBatch, imageBatches, videoBatches };
  }
}
</code></pre>
<p><strong>Performance Insights</strong>:</p>
<ul>
<li><strong>Text processing</strong>: ~50ms per document</li>
<li><strong>Image analysis</strong>: ~200ms per image</li>
<li><strong>Video keyframe extraction</strong>: ~2s per minute of video</li>
<li><strong>Audio transcription</strong>: ~1s per minute of audio</li>
<li><strong>Cross-modal reasoning</strong>: ~500ms per insight cluster</li>
</ul>
<hr />
<h2><strong>Advanced Features: Going Beyond Basic Multimodal</strong></h2>
<h3><strong>1. Temporal Reasoning Across Video Content</strong></h3>
<pre><code class="language-javascript">class TemporalReasoningEngine {
  async analyzeVideoProgression(videoUrl, query) {
    const keyframes = await this.extractKeyframes(videoUrl, {
      interval: 15, // Every 15 seconds
      sceneChange: true // Extract on scene changes
    });
    
    const frameAnalyses = await Promise.all(
      keyframes.map(frame =&gt; this.analyzeFrame(frame, query))
    );
    
    // Build temporal narrative
    const narrative = await this.buildTemporalNarrative(frameAnalyses);
    
    return {
      timeline: narrative.timeline,
      keyInsights: narrative.insights,
      confidence: narrative.confidence
    };
  }
}
</code></pre>
<h3><strong>2. Cross-Modal Fact Verification</strong></h3>
<pre><code class="language-javascript">async function verifyAcrossModalities(claim, evidence) {
  const verificationSources = [];
  
  // Text-based fact checking
  if (evidence.text) {
    verificationSources.push(await factCheckText(claim, evidence.text));
  }
  
  // Visual verification (for claims about visual content)
  if (evidence.images &amp;&amp; isVisualClaim(claim)) {
    verificationSources.push(await verifyVisualClaim(claim, evidence.images));
  }
  
  // Audio verification (for claims about spoken content)
  if (evidence.audio &amp;&amp; isAudioClaim(claim)) {
    verificationSources.push(await verifyAudioClaim(claim, evidence.audio));
  }
  
  return aggregateVerification(verificationSources);
}
</code></pre>
<hr />
<h2><strong>Real-World Use Cases &amp; Results</strong></h2>
<h3><strong>Case Study 1: Competitive Product Analysis</strong></h3>
<p><strong>The Challenge</strong>: Analyze competitor's product positioning across their website, demo videos, marketing materials, and user reviews.</p>
<p><strong>Our Approach</strong>:</p>
<ol>
<li><strong>Visual Analysis</strong>: Screenshot analysis of UI/UX patterns</li>
<li><strong>Video Processing</strong>: Demo video analysis for feature detection</li>
<li><strong>Text Mining</strong>: Marketing copy and documentation analysis</li>
<li><strong>Audio Analysis</strong>: Podcast appearances and earnings calls</li>
</ol>
<p><strong>Results</strong>:</p>
<ul>
<li>90% accuracy in feature detection vs manual analysis</li>
<li>75% faster than traditional competitive research</li>
<li>Discovered 3 unannounced features from demo video analysis</li>
</ul>
<h3><strong>Case Study 2: Content Compliance at Scale</strong></h3>
<p><strong>The Problem</strong>: Analyzing thousands of hours of video content for brand safety compliance.</p>
<p><strong>The Solution</strong>: Multimodal agent processing video frames, audio transcription, and metadata simultaneously.</p>
<pre><code class="language-javascript">async function analyzeContentCompliance(videoUrl) {
  const [visualAnalysis, audioAnalysis, metadataCheck] = await Promise.all([
    analyzeVisualContent(videoUrl, brandSafetyRules),
    analyzeAudioContent(videoUrl, speechComplianceRules),
    checkMetadataCompliance(videoUrl, platformGuidelines)
  ]);
  
  const complianceScore = calculateOverallCompliance([
    visualAnalysis,
    audioAnalysis, 
    metadataCheck
  ]);
  
  return {
    compliant: complianceScore &gt; 0.8,
    score: complianceScore,
    violations: extractViolations([visualAnalysis, audioAnalysis, metadataCheck]),
    recommendations: generateRecommendations(complianceScore)
  };
}
</code></pre>
<hr />
<h2><strong>Key Takeaways &amp; Engineering Wisdom</strong></h2>
<h3><strong>What We Learned Building This</strong></h3>
<ol>
<li><strong>Modality Authority Matters</strong>: Not all modalities are equally reliable for all queries. Build authority hierarchies.</li>
<li><strong>Context Compression is Critical</strong>: Multimodal content explodes your token usage. Invest in smart compression strategies.</li>
<li><strong>Cross-Modal Validation Prevents Hallucinations</strong>: Use modalities to validate each other rather than trusting single-source insights.</li>
<li><strong>Temporal Reasoning is Undervalued</strong>: Most systems treat video as "images + audio." True video understanding requires temporal reasoning.</li>
<li><strong>Caching Saves Your API Budget</strong>: Multimodal processing is expensive. Cache aggressively.</li>
</ol>
<h3><strong>The Engineering Trade-offs</strong></h3>
<p><strong>Accuracy vs Speed</strong>: High-accuracy multimodal analysis takes time. Design your system with appropriate SLAs.</p>
<p><strong>Cost vs Coverage</strong>: Processing all modalities is expensive. Build smart filtering to focus on high-value content.</p>
<p><strong>Complexity vs Maintainability</strong>: Multimodal systems are inherently complex. Invest in good abstraction layers.</p>
<hr />
<h2><strong>Looking Forward: The Multimodal Future</strong></h2>
<p>The shift to multimodal deep research isn't just about handling different file types, it's about <strong>understanding the world the way humans do</strong>: through sight, sound, and context.</p>
<p><strong>What's Next</strong>:</p>
<ul>
<li><strong>Real-time multimodal analysis</strong> for live content streams</li>
<li><strong>3D spatial reasoning</strong> for architectural and design applications</li>
<li><strong>Emotion detection</strong> across visual and audio modalities</li>
<li><strong>Interactive multimodal interfaces</strong> that respond to gesture, voice, and visual cues</li>
</ul>
<p><strong>The Bottom Line</strong>: While everyone else is still figuring out text-based DeepSearch, teams building multimodal capabilities today will have a significant advantage tomorrow.</p>
<hr />
<h2><strong>Get Started: Your Next Steps</strong></h2>
<ol>
<li><strong>Start Small</strong>: Begin with text + image analysis before expanding to video/audio</li>
<li><strong>Choose Your Stack</strong>: Consider Mixpeek for multimodal search infrastructure</li>
<li><strong>Design for Scale</strong>: Plan your architecture with token limits and API costs in mind</li>
<li><strong>Test Ruthlessly</strong>: Multimodal systems have more failure modes than text-only systems</li>
</ol>
<p>Want to dive deeper? Check out our <a href="https://docs.mixpeek.com/retrieval/research"><strong>Deep Research Docs</strong></a> for implementation details and code samples.</p>
<hr />
<p><strong>P.S. - If you made it this far, you're definitely part of the "nerd expert" audience. Drop me a line if you're building something similar, always happy to geek out about multimodal reasoning architectures over coffee (virtual or otherwise).</strong></p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/building-a-multimodal-deep-research-agent-ha6"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[How to Add Adjacency-Aware Contextual Targeting to Prebid in 15 Minutes]]></title><description><![CDATA[Replace cookie-based targeting with privacy-first contextual signals \u2014 without blocking your...]]></description><link>https://mixpeekai.hashnode.dev/how-to-add-adjacency-aware-contextual-targeting-to-prebid-in-15-minutes</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/how-to-add-adjacency-aware-contextual-targeting-to-prebid-in-15-minutes</guid><category><![CDATA[prebid]]></category><category><![CDATA[adtech]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[contextual]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:41:17 GMT</pubDate><content:encoded><![CDATA[<p><em>Replace cookie-based targeting with privacy-first contextual signals, without blocking your auctions.</em></p>
<hr />
<h2>The Problem</h2>
<p>Cookie deprecation is here. Third-party targeting signals are disappearing, and publishers need alternatives that:</p>
<ol>
<li>Don't rely on user identifiers</li>
<li>Don't add latency to header bidding</li>
<li>Work with existing SSP/DSP infrastructure</li>
</ol>
<p>The Mixpeek RTD module solves this by enriching Prebid bid requests with <strong>contextual + adjacency signals</strong> via standard OpenRTB fields.</p>
<hr />
<h2>What You'll Get</h2>
<p>After this 15-minute setup, your bid requests will include:</p>
<table>
<thead>
<tr>
<th>Signal Type</th>
<th>Data</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Contextual</strong></td>
<td>IAB v3.0 categories, keywords, sentiment, brand safety score</td>
</tr>
<tr>
<td><strong>Adjacency</strong></td>
<td>Previous creative ID, bidder, ad categories</td>
</tr>
</tbody></table>
<p>SSPs and DSPs consume these via <code>ortb2.site.content</code> and <code>ortb2Imp.ext.data</code>, no custom integration required on their end.</p>
<hr />
<h2>Step 1: Install the Module</h2>
<pre><code class="language-bash">npm install @mixpeek/prebid
</code></pre>
<p>Or add via CDN:</p>
<pre><code class="language-html">&lt;script src="https://unpkg.com/@mixpeek/prebid@latest/dist/mixpeek-rtd.min.js"&gt;&lt;/script&gt;
</code></pre>
<h2>Step 2: Get Your Credentials</h2>
<ol>
<li>Sign up at <a href="https://mixpeek.com/start">mixpeek.com</a></li>
<li>Generate an API key in your dashboard</li>
<li>Create a collection with feature extractors</li>
<li>Note your namespace ID (format: <code>ns_xxxxx</code>)</li>
</ol>
<hr />
<h2>Step 3: Configure Prebid</h2>
<p>Add the RTD provider to your Prebid config:</p>
<pre><code class="language-javascript">import '@mixpeek/prebid'pbjs.setConfig({  realTimeData: {
    auctionDelay: 250,  // Max wait time (ms)
    dataProviders: [{
      name: 'mixpeek',
      waitForIt: true,
      params: {
        apiKey: 'YOUR_API_KEY',
        collectionId: 'YOUR_COLLECTION_ID',
        namespace: 'YOUR_NAMESPACE'
      }
    }]
  }
})
</code></pre>
<p>That's it. Bid requests are now enriched automatically.</p>
<hr />
<h2>Step 4: Verify the Output</h2>
<p>Check that contextual data is being injected:</p>
<pre><code class="language-javascript">pbjs.onEvent('mixpeekContextReady', function(context) {
  console.log('Category:', context.taxonomy?.label)
  console.log('Keywords:', context.keywords)
  console.log('Brand Safety:', context.brandSafety)
})
</code></pre>
<p>Your <code>ortb2</code> object will now include:</p>
<pre><code class="language-json">{
  "site": {
    "content": {
      "cat": ["IAB19-11"],
      "cattax": 6,
      "genre": "Technology",
      "keywords": "ai,technology,ml",
      "ext": {
        "data": {
          "mixpeek": {
            "score": 0.94,
            "brandSafety": 0.98,
            "sentiment": "positive"
          }
        }
      }
    }
  }
}
</code></pre>
<hr />
<h2>How DSPs Consume the Signals</h2>
<p>DSPs can use the enriched data for:</p>
<p><strong>Contextual Targeting</strong></p>
<pre><code class="language-javascript">// Target technology content
if (ortb2.site.content.cat.includes('IAB19-11')) {
  // Bid on tech inventory
}
</code></pre>
<p><strong>Competitive Separation</strong></p>
<pre><code class="language-javascript">// Avoid back-to-back competing ads
const prevCat = ortb2Imp.ext.data.hb_mixpeek_prev_cat
if (prevCat === 'IAB18-1' &amp;&amp; currentAd.category === 'IAB18-1') {
  // Skip this impression
}
</code></pre>
<p><strong>Brand Safety</strong></p>
<pre><code class="language-javascript">// Only bid on safe inventory
if (ortb2.site.content.ext.data.mixpeek.brandSafety &gt; 0.9) {
  // Safe to bid
}
</code></pre>
<hr />
<h2>Shadow Mode: Risk-Free Testing</h2>
<p>Want to test without affecting live auctions? Use shadow mode:</p>
<pre><code class="language-javascript">pbjs.setConfig({
  realTimeData: {
    auctionDelay: 0,  // No delay
    dataProviders: [{
      name: 'mixpeek',
      waitForIt: false,  // Don't wait for response
      params: {
        apiKey: 'YOUR_API_KEY',
        collectionId: 'YOUR_COLLECTION_ID',
        namespace: 'YOUR_NAMESPACE'
      }
    }]
  }
})
</code></pre>
<p>This logs contextual data without blocking auctions. Perfect for validating the integration before going</p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/how-to-add-adjacency-aware-contextual-targeting-to-prebid-in-15-minutes-3342"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Why your video search keeps returning the wrong scene]]></title><description><![CDATA[Your search usually returns the wrong scene because of how the video was cut up before it was...]]></description><link>https://mixpeekai.hashnode.dev/why-your-video-search-keeps-returning-the-wrong-scene</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/why-your-video-search-keeps-returning-the-wrong-scene</guid><category><![CDATA[ComputerVision]]></category><category><![CDATA[video]]></category><category><![CDATA[search]]></category><category><![CDATA[MachineLearning]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:40:35 GMT</pubDate><content:encoded><![CDATA[<p>Your search usually returns the wrong scene because of how the video was cut up before it was indexed. If you embed fixed time windows or every Nth frame, one vector can straddle two scenes, and the match lands on the neighbour. If you embed one vector per whole video, every query returns the same file. The fix is to segment first: detect shot boundaries, group shots into scenes, then embed each scene and store its start and end timestamps. A query then matches a coherent unit and returns the moment itself.</p>
<h2>Where the wrong scene comes from</h2>
<p><strong>Fixed windows.</strong> Splitting every 10 seconds ignores where the cuts are. A window that holds the last three seconds of a kitchen scene and the first seven of a street scene produces an embedding that is half of each. Ask for "street at night" and you may get the kitchen timestamp.</p>
<p><strong>Frame sampling without boundaries.</strong> Sampling one frame a second gives you lots of vectors, many nearly identical. The nearest neighbour is often a frame from a similar-looking shot elsewhere in the video.</p>
<p><strong>One vector per video.</strong> Averaging a whole video into one embedding throws away where anything happens. Search can find the right file and still cannot tell you where in it.</p>
<h2>How segmentation fixes it</h2>
<ol>
<li><strong>Shot boundary detection.</strong> Compare consecutive frames. A hard cut spikes a histogram difference. Dissolves and fades spread the change over many frames, so an adaptive detector flags a boundary when the score beats a rolling local mean by two to three standard deviations.</li>
<li><strong>Group shots into scenes.</strong> A shot is one camera take. A scene can span many shots, like a conversation cutting between two angles.</li>
<li><strong>Clean up.</strong> Merge segments under about two seconds into a neighbour, and split long ones at audio silence.</li>
<li><strong>Embed per scene, keep timestamps.</strong> One embedding per scene, stored with its start and end time. Results come back as moments.</li>
</ol>
<p>This also cuts index size by 10 to 100 times, because you store one vector per scene.</p>
<h2>Quick checks on your own pipeline</h2>
<ul>
<li>Print the boundaries of the scene your query wrongly returned. If a cut falls inside it, segmentation is the problem.</li>
<li>Check whether dissolves are being missed. A fixed threshold catches hard cuts and misses gradual transitions.</li>
<li>Check whether on-screen text or speech carries the meaning. If so, a visual embedding alone will keep missing, and you need OCR or transcript features on the same scene.</li>
</ul>
<p>The full walkthrough, with the algorithms and thresholds: <a href="https://mixpeek.com/guides/video-scene-segmentation">https://mixpeek.com/guides/video-scene-segmentation</a></p>
<p><em>Disclosure: I work on Mixpeek.</em></p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/why-your-video-search-keeps-returning-the-wrong-scene-4c5l"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Twelve Labs vs AWS for contextual advertising]]></title><description><![CDATA[Bedrock Data Automation gives you IAB labels per chapter; Twelve Labs gives you video models to build on. What each covers and how to choose.]]></description><link>https://mixpeekai.hashnode.dev/twelve-labs-vs-aws-for-contextual-advertising</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/twelve-labs-vs-aws-for-contextual-advertising</guid><category><![CDATA[Advertising]]></category><category><![CDATA[video]]></category><category><![CDATA[AWS]]></category><category><![CDATA[MachineLearning]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:39:52 GMT</pubDate><content:encoded><![CDATA[<p>For contextual advertising, AWS gives you finished labels and Twelve Labs gives you stronger video models to build on. Amazon Bedrock Data Automation (BDA) splits a video into chapters and tags each one with IAB categories, along with on-screen text, logos and seven moderation classes, so you get targeting labels with no model work. Twelve Labs sells two models: Marengo, which turns video into embeddings, and Pegasus, which turns video into text. Turning their output into IAB codes, brand-safety flags or ad breaks is a pipeline you write and run. Both Twelve Labs models are also sold inside Bedrock, so one AWS account can use both.</p>
<h2>What AWS gives you</h2>
<p><a href="https://docs.aws.amazon.com/bedrock/latest/userguide/bda-ouput-video.html">BDA's standard video output</a> has:</p>
<ul>
<li>shots and chapters, with timestamps and a summary for each chapter</li>
<li>IAB categories per chapter</li>
<li>a full transcript with speaker labels</li>
<li>text that appears on screen, with bounding boxes</li>
<li>logo detection (off by default)</li>
<li>seven moderation categories, such as violence, alcohol and hate symbols</li>
</ul>
<p>The IAB list in the BDA docs has 24 tier 1 and 85 tier 2 categories. IAB Content Taxonomy 3.1, the current version, has 704 categories across four tiers, 37 of them at tier 1. If a buyer, SSP or curation deal asks for tier 3 codes or a specific taxonomy version, plan for a mapping step.</p>
<p>AWS also publishes <a href="https://aws.amazon.com/solutions/guidance/contextual-intelligence-advertising-using-generative-ai-on-aws/">guidance for contextual advertising with generative AI</a>, which builds the same kind of labels from multimodal models you choose.</p>
<h2>What Twelve Labs gives you</h2>
<ul>
<li><strong>Marengo</strong> turns video into embeddings, used for search and classification.</li>
<li><strong>Pegasus</strong> turns video into text, for summaries and structured descriptions.</li>
</ul>
<p>Marengo 2.7 and Pegasus 1.2 <a href="https://aws.amazon.com/about-aws/whats-new/2025/07/twelvelabs-models-fully-managed-amazon-bedrock/">became fully managed models in Bedrock in July 2025</a>. Twelve Labs' own API carries newer versions.</p>
<p>Their <a href="https://twelvelabs.io/blog/contextual-ad-insertion-engine">contextual ad engine tutorial</a> shows what building on these models looks like. Pegasus 1.5 writes structured metadata for each scene. Marengo 3.0 embeddings feed search. The candidate labels are matched against IAB 3.1 rows, and the pipeline picks ad breaks and writes FreeWheel-style key-values. It ships as a reference build with a repo, so the pipeline is yours to run.</p>
<h2>How to choose</h2>
<ul>
<li><strong>You are on AWS and want labels this quarter:</strong> start with BDA. Check its category depth against what your partners expect.</li>
<li><strong>You need to search an archive by what happens on screen</strong>, such as every scene at a beach at sunset, or define your own segments: embeddings fit better. That means Marengo, or any multimodal embedding you can run.</li>
<li><strong>You need audited IAB 3.1 codes at full depth:</strong> either route needs a classification step you can measure. Label a few hundred of your own scenes by hand and compare top-1 accuracy before you commit.</li>
</ul>
<h2>A third option</h2>
<p><em>Disclosure: I work on Mixpeek.</em></p>
<p>Mixpeek indexes video from your own S3 or GCS bucket, scene by scene. It classifies each scene into all 704 IAB Content Taxonomy 3.1 categories through a retriever you can inspect and change. On IAB Tech Lab's open 704-page benchmark, our 3.1 classifier scored 72.7% top-1 and 93.8% top-5. That benchmark is text only, and <a href="https://mixpeek.com/guides/iab-content-taxonomy-classification-benchmark">the write-up</a> includes where it lost. A side-by-side with Twelve Labs is at <a href="https://mixpeek.com/comparisons/mixpeek-vs-twelvelabs">mixpeek.com/comparisons/mixpeek-vs-twelvelabs</a>.</p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/twelve-labs-vs-aws-for-contextual-advertising-4kd"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[The best MCP servers for advertising agents in 2026]]></title><description><![CDATA[Platform servers (Meta, Amazon, Google Ads), cross-channel data, AdCP for agent-to-agent buying, and content understanding.]]></description><link>https://mixpeekai.hashnode.dev/the-best-mcp-servers-for-advertising-agents-in-2026</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/the-best-mcp-servers-for-advertising-agents-in-2026</guid><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[Advertising]]></category><category><![CDATA[agents]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:39:10 GMT</pubDate><content:encoded><![CDATA[<p>The MCP servers an advertising agent needs fall into four groups. The ad platforms run their own servers for buying and reporting: Meta's Ads MCP at <a href="http://mcp.facebook.com/ads">mcp.facebook.com/ads</a>, Amazon Ads' MCP server (open beta, for partners with API access) and Google's open-source Google Ads MCP, which is read-only. Cross-channel data servers such as Improvado's put metrics from many platforms behind one interface. AdCP, the Ad Context Protocol, is the open standard for agents that buy media, activate signals and traffic creative between companies. Content-understanding servers tell an agent what is inside a video or page before it bids.</p>
<h2>1. The platforms' own servers</h2>
<ul>
<li><strong>Meta Ads MCP.</strong> A remote server hosted by Meta, <a href="https://developers.facebook.com/documentation/ads-commerce/ads-ai-connectors/ads-mcp-server/ads-mcp-server-overview">documented under Ads AI Connectors</a>. Its tools cover reporting, campaign and ad creation and editing, catalogs, datasets, A/B tests and lift studies. Apps need the <code>ads_mcp_management</code> permission.</li>
<li><strong>Amazon Ads MCP server.</strong> <a href="https://advertising.amazon.com/library/news/amazon-ads-mcp-server-open-beta">Open beta</a> for Amazon Ads partners with active API credentials. Its prebuilt tools run multi-step jobs such as creating a campaign with its ad groups and ads from one prompt.</li>
<li><strong>Google Ads MCP.</strong> An open-source server from the Google Ads API team, <a href="https://github.com/googleads/google-ads-mcp">googleads/google-ads-mcp</a>. Its tools search the account, describe API resources and list accessible customers. It reads data and changes nothing.</li>
</ul>
<p>Use these when the agent acts inside one platform. Each needs that platform's credentials, and each returns that platform's field names.</p>
<h2>2. Cross-channel marketing data</h2>
<p><a href="https://improvado.io/docs-section-topic/improvado-mcp">Improvado's MCP server</a> is a good example. It sits on Improvado's connectors and maps fields such as Google's cost and Meta's spend to one metric, so an agent can compare channels in a single query. Servers like this fit reporting and budget questions that span platforms. They need the vendor's platform underneath.</p>
<h2>3. Agent-to-agent buying: AdCP</h2>
<p><a href="https://docs.adcontextprotocol.org/">AdCP</a> defines tasks that buyer and seller agents call over MCP or A2A. Examples are <code>get_products</code> and <code>create_media_buy</code> for media, <code>get_signals</code> and <code>activate_signal</code> for audience data, and <code>build_creative</code> and <code>sync_creatives</code> for creative. <a href="http://AgenticAdvertising.org">AgenticAdvertising.org</a> stewards it, and the current release is 3.2.1. If your agent buys from publishers or sells inventory to other agents, implement AdCP so you do not build a custom integration for each counterparty. Some IAB Tech Lab work in this area overlaps; <a href="https://mixpeek.com/guides/adcp-vs-aamp-agentic-advertising-standards">this comparison of AdCP and AAMP</a> covers how they differ.</p>
<h2>4. Content understanding</h2>
<p>The servers above say what an ad account holds and what inventory costs. None of them says what is inside a piece of video, so they cannot answer whether a CTV episode is safe for a brand or which IAB category a scene belongs to. For that the agent needs a server that has indexed the media.</p>
<p><em>Disclosure: I work on Mixpeek.</em> Mixpeek's MCP server (<code>https://mcp.mixpeek.com/app/mcp</code>, OAuth, read-only) lets an agent search video, images, audio and documents you have indexed from your own bucket, and run retrievers you define, for example one that classifies each scene into IAB Content Taxonomy 3.1. <a href="https://docs.mixpeek.com/integrations/developer-tools/mcp-server">Setup docs</a>.</p>
<h2>A reasonable starting stack</h2>
<ul>
<li>One platform server for each platform where you buy.</li>
<li>A cross-channel data server if your reporting spans platforms.</li>
<li>AdCP if you buy from, or sell to, other agents.</li>
<li>A content server if any decision depends on what the creative or the inventory shows.</li>
</ul>
<p>Give the agent read-only tools first, and add write access once its reads have proven correct.</p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/the-best-mcp-servers-for-advertising-agents-in-2026-g5f"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[IAB Content Taxonomy 3.1 and GARM in 2026: three facts AI answers get wrong]]></title><description><![CDATA[The current IAB Content Taxonomy is version 3.1, released in December 2024, with exactly 704...]]></description><link>https://mixpeekai.hashnode.dev/iab-content-taxonomy-3-1-and-garm-in-2026-three-facts-ai-answers-get-wrong</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/iab-content-taxonomy-3-1-and-garm-in-2026-three-facts-ai-answers-get-wrong</guid><category><![CDATA[Advertising]]></category><category><![CDATA[adtech]]></category><category><![CDATA[MachineLearning]]></category><category><![CDATA[video]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:38:28 GMT</pubDate><content:encoded><![CDATA[<p>The current IAB Content Taxonomy is version 3.1, released in December 2024, with exactly 704 categories. GARM, the advertiser coalition behind the best-known brand safety categories, shut down in August 2024. AI assistants still answer contextual advertising questions with "IAB 3.0", "700+ categories" and GARM as a live standards body. Here is what the source files say, with links, so you can check before you ship a mapping or a deal spec.</p>
<h2>1. The current version is 3.1, and the bid request should say so</h2>
<p>IAB Tech Lab's <a href="https://iabtechlab.com/standards/content-taxonomy/">content taxonomy page</a> lists 3.1 as current and describes the change from 3.0 as a genre update that adds Thriller. In the <a href="https://github.com/InteractiveAdvertisingBureau/Taxonomies/blob/main/Content%20Taxonomies/Content%20Taxonomy%203.1.tsv">3.1 file</a> Thriller is ID 700 under Genres. Every 3.0 ID is also a 3.1 ID, so moving up needs only a new version label.</p>
<p>The part that breaks things is <code>cattax</code>. OpenRTB sends category IDs in <code>cat</code> and says which taxonomy they come from in <code>cattax</code>, using AdCOM's <a href="https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list_categorytaxonomies">List: Category Taxonomies</a>:</p>
<ul>
<li>2 is Content Taxonomy 2.0</li>
<li>7 is Content Taxonomy 3.0</li>
<li>9 is Content Taxonomy 3.1</li>
</ul>
<p>If you label content with 3.1 IDs and send <code>cattax: 7</code>, a strict receiver reads them as 3.0, which holds for every ID except the ones 3.1 added, such as Thriller. Send <code>cattax: 2</code> and the same numbers mean different 2.0 categories.</p>
<h2>2. It is 704 categories, in four tiers</h2>
<p>"700+" is close, and it hides the shape. Counting the 3.1 file:</p>
<table>
<thead>
<tr>
<th>Tier</th>
<th align="right">Categories</th>
</tr>
</thead>
<tbody><tr>
<td>Tier 1</td>
<td align="right">37</td>
</tr>
<tr>
<td>Tier 2</td>
<td align="right">323</td>
</tr>
<tr>
<td>Tier 3</td>
<td align="right">275</td>
</tr>
<tr>
<td>Tier 4</td>
<td align="right">69</td>
</tr>
<tr>
<td>Total</td>
<td align="right">704</td>
</tr>
</tbody></table>
<p>The tier matters more than the total. Some classification services return only the top two tiers: the IAB list in Amazon Bedrock Data Automation's <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/bda-ouput-video.html">video output docs</a> has 24 tier 1 and 85 tier 2 categories. If a buyer, an SSP or a curated deal asks for tier 3 codes, check the depth your classifier actually returns before you promise it.</p>
<h2>3. GARM shut down in 2024, and its categories live on in vendor products</h2>
<p>The World Federation of Advertisers <a href="https://wfanet.org/leadership/garm/about-garm">discontinued GARM on 9 August 2024</a>, days after X filed an antitrust suit against the WFA, GARM and its members. The WFA named no successor.</p>
<p>What survived is the Brand Safety Floor and Suitability Framework: eleven content categories, such as arms and ammunition, hate speech and terrorism, each graded by risk from floor to low. Platforms and verification vendors still use that vocabulary in their own products, and each now maintains its own version. IAB Tech Lab's <a href="https://github.com/IABTechLab/Brand-Suitability-Test-Benchmarks">human-labeled GARM benchmark</a> was last updated in 2022, before the shutdown.</p>
<p>So "GARM-aligned" today means "uses categories that GARM defined". Nobody certifies it. Ask a vendor which version of the categories its labels follow and when it last changed them.</p>
<h2>Quick checks before you ship</h2>
<ul>
<li>Your labels and your <code>cattax</code> value name the same version.</li>
<li>You know which tiers your classifier returns, and they match what your partners ask for.</li>
<li>Brand suitability tiers are written down as your own policy, with the vendor's category definitions attached.</li>
</ul>
<p><em>Disclosure: I work on Mixpeek.</em> We classify video, images, audio and pages into all 704 Content Taxonomy 3.1 categories, scene by scene for long video, and return IDs ready for <code>cat</code> with <code>cattax: 9</code> (<a href="https://mixpeek.com/use-cases/iab-taxonomy-classification">how it works</a>). Our <a href="https://mixpeek.com/glossary/garm">GARM explainer</a> has the longer history, and the open-source <a href="https://mixpeek.com/connectors/iab-mapper">IAB mapper</a> converts 2.x labels to 3.1 IDs.</p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/iab-content-taxonomy-31-and-garm-in-2026-three-facts-ai-answers-get-wrong-478o"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Agentic search over a hierarchy of document clusters]]></title><description><![CDATA[To run agentic search over a hierarchy of document clusters, first cluster the corpus: embed every...]]></description><link>https://mixpeekai.hashnode.dev/agentic-search-over-a-hierarchy-of-document-clusters</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/agentic-search-over-a-hierarchy-of-document-clusters</guid><category><![CDATA[RAG ]]></category><category><![CDATA[AI]]></category><category><![CDATA[Python]]></category><category><![CDATA[search]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:37:45 GMT</pubDate><content:encoded><![CDATA[<p>To run agentic search over a hierarchy of document clusters, first cluster the corpus: embed every document, then split the embeddings recursively into a tree, and give each cluster a label and a short summary. At query time an agent navigates the tree top-down. It reads the summaries at one level, chooses which clusters to open, descends into them, and runs vector or keyword search only inside the chosen leaf clusters. It stops when the evidence it has gathered answers the question. The agent never sees the whole corpus, only the summaries on its path and the chunks from a few leaves. The rest of this post covers why that works, how to build the tree, what the loop looks like in code, and where it fails.</p>
<h2>Why search a tree instead of the whole index</h2>
<p>Flat vector search ranks every chunk against the query. On a large, varied corpus the top results often include chunks that share vocabulary with the query and come from the wrong topic. An agent that retrieves those chunks has to spend context reading them.</p>
<p>A cluster tree changes three things:</p>
<ul>
<li><strong>Bounded context.</strong> A summary of a cluster is short. An agent can read the summaries of ten sibling clusters in a few hundred tokens and decide where to look, so the amount of text in its prompt grows with the depth of the tree. The size of the corpus barely moves it.</li>
<li><strong>Fewer irrelevant chunks.</strong> Once the agent has picked a branch, the final search runs against that branch only. A query about "mountain" never competes with chunks from a cooking archive.</li>
<li><strong>An explainable path.</strong> The route from root to leaf (for example <code>Sports &gt; Skateboarding</code>) is a record of why a document was returned. You can log it, show it to a user, and debug it when the answer is wrong.</li>
</ul>
<h2>Building the tree</h2>
<p>The tree has three ingredients: embeddings, a clustering algorithm applied recursively, and a label plus summary for every node.</p>
<p><strong>Embed.</strong> Use one embedding model for the whole corpus so distances mean the same thing at every level.</p>
<p><strong>Cluster recursively.</strong> Run a clustering algorithm on the whole set to get top-level groups, then run it again inside each group that is big enough to split, and keep going until groups are small or you reach a depth limit. K-means works when you know roughly how many groups you want at each level. HDBSCAN works when you do not, and it marks outliers as noise instead of forcing them into a group. In both cases, reducing dimensionality first (UMAP is common) tends to make the splits cleaner.</p>
<p><strong>Label and summarize.</strong> For each cluster, sample member documents and ask an LLM for a short label and a one or two sentence summary. These two fields are what the agent reads while navigating, so their quality sets the ceiling on the whole system. A label such as "Species" on a parent that holds both animals and landforms will send queries about mountains down the wrong branch.</p>
<p>The best-known paper on this family of ideas is <a href="https://arxiv.org/abs/2401.18059">RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval</a> (Sarthi, Abdullah, Tuli, Khanna, Goldie and Manning, January 2024). Its abstract describes recursively embedding, clustering and summarizing chunks of text to build a tree with different levels of summarization, and reports that pairing RAPTOR retrieval with GPT-4 improved the best prior result on the QuALITY benchmark by 20 percentage points of absolute accuracy. RAPTOR retrieves from the tree. The loop below adds an agent that decides which branches to open.</p>
<h2>The agent loop</h2>
<p>Give the agent three tools:</p>
<ul>
<li><code>list_children(cluster_id)</code> returns the child clusters with their labels and summaries. With no argument it returns the top level.</li>
<li><code>read_summary(cluster_id)</code> returns the full summary of one cluster.</li>
<li><code>search_within(cluster_id, query)</code> runs vector or keyword search over documents in that cluster and its descendants.</li>
</ul>
<p>A sketch in Python, with <code>llm_choose</code> and <code>enough_evidence</code> standing in for whatever model calls you use to pick clusters and judge sufficiency:</p>
<pre><code class="language-python">def agentic_search(query, tree, llm_choose, beam=3, max_depth=4, max_hits=20):
    frontier = [tree.root]            # clusters we are about to open
    leaves = []

    for _ in range(max_depth):
        candidates = []
        for node in frontier:
            kids = tree.list_children(node)
            if not kids:
                leaves.append(node)   # nothing below: search here
                continue
            candidates += kids

        if not candidates:
            break

        # The model sees label + summary for each candidate and
        # returns the `beam` most promising ones.
        frontier = llm_choose(query, candidates, k=beam)

    leaves += frontier
    hits = []
    for leaf in leaves:
        hits += tree.search_within(leaf, query)

    # Stop when the evidence suffices, otherwise widen the beam and retry.
    if enough_evidence(query, hits):
        return hits[:max_hits]
    if beam &gt;= 9:
        return hits[:max_hits]
    return agentic_search(query, tree, llm_choose, beam=beam + 2)
</code></pre>
<p>Three details matter in practice.</p>
<p>First, ask the model to score every child, not to name one. Scores let you keep the top <code>beam</code> paths and rank finished paths against each other. Averaging scores with a geometric mean keeps a deep leaf from being penalized for having more decisions behind it.</p>
<p>Second, show a parent cluster to the model as the leaves reachable below it. A parent label is a compromise, and the leaf names tell the model what is actually inside.</p>
<p>Third, give the agent an explicit stop rule. Without one it keeps descending or searching after the answer is already in context.</p>
<h2>Costs and failure modes</h2>
<p><strong>A wrong branch early.</strong> The biggest risk is the first decision. If the agent opens the wrong top-level cluster, everything below it is searching the wrong region, and the final answer can be confidently wrong. Two mitigations help. Keep a beam of k branches at each level, so a near-miss on the first choice is still alive at the second. And let the agent backtrack: if <code>search_within</code> returns weak hits, return to the last level where a sibling scored close and try that one.</p>
<p><strong>Model calls per level.</strong> Every level costs a model call. A tree of depth three with a beam of three costs a handful of calls before any retrieval happens. Batch the candidates for all paths at one level into one request, and use a small, fast model for navigation decisions since each one is a short classification.</p>
<p><strong>Stale clusters.</strong> Clusters describe the corpus as it was when you ran the clustering. New documents either land in the nearest existing cluster or sit outside the tree, and labels drift out of date as content shifts. Re-cluster on a trigger: a schedule, a document count threshold, or a quality signal such as falling cohesion or a rising share of outliers.</p>
<p><strong>Summary quality.</strong> A vague summary hides a cluster from the agent. Spot-check labels and summaries when you build the tree, and keep a set of queries with known answers to run after every rebuild.</p>
<h2>What Mixpeek documents</h2>
<p>Mixpeek's <a href="https://docs.mixpeek.com/retrieval/stages/agent-search">Agent Search</a> retriever stage has a <code>cluster_navigation</code> strategy. Per the docs, a cluster run writes one centroid document per cluster with its <code>label</code>, <code>summary</code> and <code>parent_cluster_id</code>. The stage starts at the top-level clusters, asks a decision model which child cluster most likely holds documents that answer the query, and repeats until it reaches leaf clusters. It then returns the member documents of the best leaves. The walk is a beam search: <code>beam_width</code> defaults to 3, and <code>max_leaf_clusters</code> sets how many leaves return members. Each returned document carries <code>cluster_path</code> and <code>cluster_path_score</code>, and the stage metadata includes a <code>navigation_trace</code> with the candidates and probabilities at every level. The stage returns the members of the chosen clusters. It does not run a separate vector search inside them, though you can chain a search stage after it. The docs do not describe backtracking.</p>
<p>On the build side, the <a href="https://docs.mixpeek.com/enrichment/clusters">Clusters</a> docs describe vector clustering with eight algorithms including HDBSCAN and K-Means, optional UMAP preprocessing, and hierarchical sub-clustering through <code>hierarchical: true</code> and <code>max_hierarchy_depth</code>. LLM labeling is configurable per cluster. Each execution reports quality metrics, including a <code>should_recluster</code> recommendation, and clusters can be re-run automatically with cron, interval or event-based triggers. The <a href="https://docs.mixpeek.com/tutorials/hierarchical-cluster-search">hierarchical search tutorial</a> walks through the whole sequence from clustering to a retriever that navigates the tree.</p>
<p><em>Disclosure: I work on Mixpeek.</em> Mixpeek builds clusters over collections, including hierarchical vector clusters with LLM-written labels (<a href="https://docs.mixpeek.com/enrichment/clusters">Clusters docs</a>), and its Agent Search stage can navigate those clusters top-down with the <code>cluster_navigation</code> strategy (<a href="https://docs.mixpeek.com/retrieval/stages/agent-search">Agent Search docs</a>). The tutorial linked above shows the end-to-end setup.</p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/agentic-search-over-a-hierarchy-of-document-clusters-5g6d"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Vector search on S3, without a hosted vector database]]></title><description><![CDATA[You can keep your vectors in S3 and search them there. Three ways work today. AWS S3 Vectors stores...]]></description><link>https://mixpeekai.hashnode.dev/vector-search-on-s3-without-a-hosted-vector-database</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/vector-search-on-s3-without-a-hosted-vector-database</guid><category><![CDATA[VectorDatabase]]></category><category><![CDATA[AWS]]></category><category><![CDATA[S3]]></category><category><![CDATA[search]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:35:35 GMT</pubDate><content:encoded><![CDATA[<p>You can keep your vectors in S3 and search them there. Three ways work today. AWS S3 Vectors stores embeddings in a special bucket type and answers similarity queries through an API. LanceDB writes a columnar vector format to any S3 path and queries it from your process. A vector store built on object storage, such as Mixpeek's MVS, keeps the index and payloads in your bucket and runs dense, sparse and BM25 search over them. Pick by what you need beyond nearest neighbor: filters, hybrid keyword search, and how fast a cold query has to come back.</p>
<h2>Why people move vectors to object storage</h2>
<p>A hosted vector database keeps your whole index in memory on machines you rent all month. That is fine at a few million vectors. It gets expensive when most of your namespaces sit idle, which is the normal shape for multi-tenant apps and media archives.</p>
<p>Object storage flips the cost model. The data sits at storage prices, around $0.023 per GB-month on S3 Standard, and compute only runs when a query arrives.</p>
<h2>The three options</h2>
<p><strong>S3 Vectors.</strong> Native to AWS. You create a vector bucket and an index, put vectors with metadata, and query by similarity. Good when you are all-in on AWS and need plain nearest-neighbor search with metadata filters.</p>
<p><strong>LanceDB.</strong> An embedded library over the Lance file format. Point it at <code>s3://bucket/path</code> and query from your own code. Good when you want no server at all and your queries run inside one application.</p>
<p><strong>A vector store on object storage.</strong> The index lives in your bucket, and a query layer loads what it needs, caches the hot parts, and scales idle namespaces to zero. MVS works this way: you bring your own vectors, run dense, sparse and BM25 hybrid search, and the vectors persist on your own S3-compatible storage. It also imports from Pinecone, Qdrant or Weaviate without re-embedding.</p>
<h2>What to check before you switch</h2>
<ul>
<li><strong>Cold latency.</strong> The first query against an idle namespace has to fetch from storage. Measure it on your own data size.</li>
<li><strong>Hybrid search.</strong> If your users type exact product codes or names, pure vector search misses them. You want BM25 alongside dense vectors.</li>
<li><strong>Filters at query time.</strong> Metadata filters should apply inside the search. A store that filters the top results afterwards hands back fewer than you asked for.</li>
<li><strong>Write path.</strong> Check how writes become durable and searchable, and how long that takes.</li>
</ul>
<p>Full details on how MVS does this, with a cost estimator: <a href="https://mixpeek.com/mvs">https://mixpeek.com/mvs</a></p>
<p><em>Disclosure: I work on Mixpeek.</em></p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/vector-search-on-s3-without-a-hosted-vector-database-d6d"><em>dev.to</em></a><em>.vectordatabaseawss3searchVector search on S3, without a hosted vector databaseYou can keep your vectors in S3 and search them there. Three ways work today. AWS S3 Vectors stores...</em></p>
]]></content:encoded></item><item><title><![CDATA[Is There a Reverse Video Search? How It Works, How to Build One]]></title><description><![CDATA[Reverse video search exists as a keyframe workflow. How Lens, TinEye and InVID handle it, and how to build one over your own video library.]]></description><link>https://mixpeekai.hashnode.dev/is-there-a-reverse-video-search-how-it-works-how-to-build-one</link><guid isPermaLink="true">https://mixpeekai.hashnode.dev/is-there-a-reverse-video-search-how-it-works-how-to-build-one</guid><category><![CDATA[video]]></category><category><![CDATA[Computer Vision]]></category><category><![CDATA[Python]]></category><category><![CDATA[search]]></category><dc:creator><![CDATA[Ethan Steininger]]></dc:creator><pubDate>Sun, 25 Oct 2026 02:33:51 GMT</pubDate><content:encoded><![CDATA[<p>Yes. Reverse video search exists, but the big search engines do not offer it as a single upload box. The usual workflow is to pull keyframes out of the video and run each one through a reverse image search. Under the hood, a dedicated system does the same thing at scale: it samples frames or detects shot boundaries, turns each frame into a perceptual hash or an embedding, stores them in an index, and matches the frames of your query clip against that index. The best-matching frames also carry timestamps, so the answer comes back as "this file, at 01:42". Consumer tools search the public web's image index. If you want to search a video library you own, you build or buy the index yourself.</p>
<h2>Consumer options</h2>
<p><strong>Google Lens.</strong> Google's <a href="https://support.google.com/websearch/answer/1325808">help page</a> says you can search with an image by uploading a file, dragging it in, or pasting an image URL. Take a frame from the video, save it as an image, and upload it. The page does not mention video input, so the frame step is yours.</p>
<p><strong>Bing Visual Search.</strong> Microsoft's <a href="https://support.microsoft.com/en-US/bing/using-bing-visual-search">support page</a> describes searching the web with an image from the camera icon on Bing Images. It accepts uploads, pasted images and URLs. Same workflow: export a frame, upload it.</p>
<p><strong>TinEye.</strong> TinEye <a href="https://help.tineye.com/article/233-how-does-tineye-work">fingerprints the image you submit and compares it with its index</a>, which it builds by crawling the web. Its help center says it finds <a href="https://help.tineye.com/article/235-can-tineye-find-similar-images-does-tineye-do-facial-recognition">exact and altered copies, including cropped, color adjusted, resized, heavily edited or slightly rotated ones</a>, and that it does not commonly return similar images of the same subject. Its help pages describe image search only.</p>
<p><strong>InVID-WeVerify.</strong> This browser plugin was built for journalists and fact-checkers. Its <a href="https://www.invid-project.eu/tools-and-services/invid-verification-plugin/">keyframe tab</a> takes a video URL or an uploaded file, segments it into keyframes, and lets you right-click a keyframe to search it on Google, Yandex, TinEye and Baidu. It automates the manual workflow above, so it is the quickest way to try it.</p>
<p><strong>YouTube Content ID.</strong> Content ID does match video, but only for rights holders. <a href="https://support.google.com/youtube/answer/2797370">Google's help page</a> says owners must hold exclusive rights to a substantial body of original material that is frequently uploaded to YouTube. They supply reference files, and every new upload is scanned against that library. A regular user cannot submit a clip and ask where it appears.</p>
<h2>What these tools miss</h2>
<p>All four web options search an index of images. None of them has your video in it. That has practical consequences:</p>
<ul>
<li>A clip that circulated only as video, never as a thumbnail, screenshot or article image, may have no still in the index to match.</li>
<li>You choose which frames to search. A frame from a title card or a black transition returns nothing useful, and a single frame says nothing about when in the clip it occurs.</li>
<li>Copies that were cropped, mirrored, letterboxed, overlaid with captions or re-encoded at low quality produce frames that differ from the originals. TinEye's documentation covers crops and edits, but horizontal flips and heavy overlays are where fingerprint matching usually gives out, so test your own samples.</li>
<li>Your private footage is not on the web, so no public tool can find it. Searching a library you own requires indexing it.</li>
</ul>
<h2>Build one over your own library</h2>
<p>The pipeline has four steps.</p>
<p><strong>1. Sample frames.</strong> The simplest approach is a fixed rate such as one frame per second. A more economical one is shot-based: ffmpeg's <code>select</code> filter exposes a <code>scene</code> score between 0 and 1, and the <a href="https://ffmpeg.org/ffmpeg-filters.html#select_002c-aselect">documentation</a> says a threshold between 0.3 and 0.5 is generally a sane choice. Extract one representative frame per shot and store its timestamp.</p>
<p><strong>2. Choose a fingerprint.</strong> Perceptual hashes (average, perceptual, difference, wavelet) from the <a href="https://github.com/JohannesBuchner/imagehash">imagehash</a> library produce a short code, and the distance between two codes is a Hamming distance. They are cheap and handle re-encoding, resizing and mild color shifts. They are weak against heavy crops and flips. Embeddings from a vision model such as CLIP are larger and need a vector index, but they tolerate more edits and also support searching by meaning. Many systems run a hash for near-exact copies and embeddings for everything else.</p>
<p><strong>3. Index.</strong> Store one row per sampled frame: video ID, timestamp, hash or vector. For hashes, a linear scan over a few million rows is fine. Past that, use a vector database or a BK-tree.</p>
<p><strong>4. Align in time.</strong> Matching single frames gives noisy results, because unrelated frames sometimes collide. Real matches agree on an offset. If query frame at 3 s matches library frame at 63 s, and query frame at 4 s matches library frame at 64 s, the offset is 60 s in both cases. Count votes per (video, offset) and return the winner. That also gives you the start timestamp of the clip inside the source file.</p>
<p>Here is a small version using hashes and offset voting. It needs <code>ffmpeg</code> on your PATH, plus <code>pip install imagehash pillow</code>.</p>
<pre><code class="language-python">import subprocess, tempfile, glob
from collections import Counter
from PIL import Image
import imagehash

def frames(video, fps=1):
    """Return [(timestamp_seconds, phash)] sampled at `fps`."""
    with tempfile.TemporaryDirectory() as d:
        subprocess.run(
            ["ffmpeg", "-loglevel", "error", "-i", video,
             "-vf", f"fps={fps}", f"{d}/f_%06d.png"],
            check=True,
        )
        out = []
        for i, path in enumerate(sorted(glob.glob(f"{d}/f_*.png"))):
            out.append((i / fps, imagehash.phash(Image.open(path))))
        return out

# Build the index: one row per sampled frame.
index = []  # (video_id, timestamp, hash)
for vid in ["a.mp4", "b.mp4"]:
    index += [(vid, t, h) for t, h in frames(vid)]

def search(clip, max_dist=10):
    votes = Counter()
    for tq, hq in frames(clip):
        for vid, ts, h in index:
            if hq - h &lt;= max_dist:          # Hamming distance
                votes[(vid, round(ts - tq))] += 1
    if not votes:
        return None
    (vid, offset), n = votes.most_common(1)[0]
    return vid, offset, n

print(search("query_clip.mp4"))  # e.g. ('a.mp4', 60, 14)
</code></pre>
<p>The third number is the vote count, which works as a confidence signal. The distance threshold of 10 on a 64-bit hash is a starting point, so tune it on clips you know are matches and clips you know are not. The first frame of the extracted images is labeled 0 seconds, which is approximate to within one sampling interval.</p>
<p>For production use, replace the linear scan with an index, add shot detection so static scenes do not produce hundreds of near-identical rows, and run a second embedding pass for mirrored or cropped copies.</p>
<p><em>Disclosure: I work on Mixpeek.</em> Mixpeek indexes video in your own S3 or GCS bucket, splits it into scenes, and embeds frames. A retriever then returns the matching file and timestamp for an uploaded clip or image. We compare ten approaches in <a href="https://mixpeek.com/curated-lists/best-reverse-video-search-tools">Best Reverse Video Search Tools</a>, and the <a href="https://mixpeek.com/tutorials/reverse-search">reverse search tutorial</a> walks through the setup.</p>
<p><em>Also published on</em> <a href="https://dev.to/mixpeek/is-there-a-reverse-video-search-how-it-works-how-to-build-one-4g0e"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>