惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
V
V2EX
C
Check Point Blog
GbyAI
GbyAI
D
Docker
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
T
Troy Hunt's Blog
博客园 - Franky
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Security Blog
Microsoft Security Blog
P
Privacy & Cybersecurity Law Blog
WordPress大学
WordPress大学
The Cloudflare Blog
S
SegmentFault 最新的问题
Latest news
Latest news
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
I
InfoQ
博客园 - 【当耐特】
NISL@THU
NISL@THU
A
About on SuperTechFans
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Scott Helme
Scott Helme
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
Security Latest
Security Latest
V
Vulnerabilities – Threatpost
Security Archives - TechRepublic
Security Archives - TechRepublic
A
Arctic Wolf
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
IT之家
IT之家
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
大猫的无限游戏
大猫的无限游戏
S
Security Affairs
The Register - Security
The Register - Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
LINUX DO - 热门话题
T
Tor Project blog

Pinecone

Pinecone Assistant: A Managed Knowledge Layer for Production AI Applications Multi-domain RAG in n8n: why one knowledge base is not enough Allspice Transforms the Culinary Experience with Semantic Search Powered by Pinecone | Pinecone Building RAG workflows in n8n: choosing the right Pinecone node Knowledge needs a meta-knowledge layer Garbage Day: How Pinecone Safely Deletes Billions of Objects at Scale When "Performance" Means Two Different Things Pinecone BYOC: Pinecone in your AWS, GCP, or Azure account, no vendor access True, Relevant, and Wrong: The Applicability Problem in RAG Use the Pinecone Plugin for Claude Code to develop AI Applications Faster Millions at Stake: How Melange's High-Recall Retrieval Prevents Litigation Collapse Powering High-stakes Patent Search at Scale: How Melange Built a Reliable AI System on Pinecone | Pinecone Pinecone Assistant Node in n8n: Turn Any Data Source Into Knowledge RAG with Access Control Pinecone Dedicated Read Nodes are now in Public Preview Inside Pinecone: Slab Architecture New Bulk Data Operations: Update, Delete, and Fetch by Metadata The Hidden Cost of Building: Lessons from Aquant Simplifying Vector Embeddings with Pinecone Integrated Inference Capabilities Pinecone joins Microsoft Marketplace as a Launch Partner GTM Engineering: Clay + Pinecone for AI-powered Sales Outbound Build an AI knowledge assistant with Google Docs and Pinecone Moving Pinecone forward with Ash Ashutosh as CEO and Edo spearheading our growing AI ambitions as Chief Scientist Pinecone Founder Edo Liberty to Spearhead Pinecone’s Growing AI Ambitions; Appoints Ash Ashutosh as CEO to Expand Vector Database Market Leadership Fast, Accurate Retrieval for Creators at Scale: Delphi’s Path Toward a Million Conversational Agents with Pinecone | Pinecone Announcing Pinecone Pioneers: A Program for Builders, Organizers, and Community Leaders What is Context Engineering? Chunking Strategies for LLM Applications Beyond the hype: Why RAG remains essential for modern AI Obviant Makes 30% More Accurate Defense Acquisition Recommendations Combining Sparse and Dense Retrieval with Pinecone | Pinecone Build more knowledgeable AI applications with new LLMs and greater control in Pinecone Assistant #NYTECHWEEK 2025 Retrieval-Augmented Generation (RAG) Accurate and Efficient Metadata Filtering in Pinecone’s Serverless Vector Database | Pinecone Terminal X AI Agents, Powered by Pinecone, Turn Complex Financial Data Into Production-grade Insights at Scale | Pinecone Aquant Delivers Scalable, Expert-level Service Intelligence with Pinecone | Pinecone Cascading retrieval with multi-vector representations: balancing efficiency and effectiveness Vector databases aren't just for large-scale enterprise AI Unveiling DIME: Reproducibility, Scalability, and Formal Analysis of Dimension Importance Estimation for Dense Retrieval | Pinecone Fast and Effective Early Termination for Simple Ranking Functions | Pinecone Domain-specific AI Agents at Scale: CustomGPT.ai Serves 10,000+ Customers with Pinecone | Pinecone Using Pinecone asynchronously with FastAPI A Flexible Resource for Top-Weighted Comparisons Between Sets and Rankings | Pinecone Build secure, scalable agentic AI workflows with Rubrik Annapurna and Pinecone Tool up: Pinecone’s first MCP servers are here Add context to your agent with Pinecone Assistant MCP remote server E2Rank: Efficient and Effective Layer-wise Reranking | Pinecone ColBERT-serve: Efficient Multi-Stage Memory-Mapped Scoring | Pinecone Efficient Constant-Space Multi-Vector Retrieval | Pinecone How Vanguard Worked with Pinecone to Boost Customer Support with Faster Calls and 12% More Accurate Responses | Pinecone Pinecone Named to Fast Company's Annual List of the World's Most Innovative Companies of 2025 Launch Week: Pinecone for agents, search, recommendations, and more Optimizing Pinecone for agents (and more) Retrieval Inference for scale and performance How 1up Turns Sales Reps Into Product Experts with Pinecone | Pinecone Don’t be dense: Launching sparse indexes in Pinecone Unlock High-Precision Keyword Search with pinecone-sparse-english-v0 Evolving Pinecone's architecture to meet the demands of Knowledgeable AI Pinpoint references faster with citation highlights in Pinecone Assistant Bringing the leading vector database to your cloud Getting started with llama-text-embed-v2 Natural Language Counterfactual Explanations for Graphs Using Large Language Models | Pinecone Easily build knowledgeable chat and agent-based applications in minutes with Pinecone Assistant, now generally available How to build an agentic, chat or RAG knowledge system using Pinecone Assistant Real-time RAG with Pinecone and Estuary Flow BigQuery to Pinecone in Real-Time with Estuary Flow Stravito Turns Market and Consumer Data Into Actionable Insights with Pinecone Inference | Pinecone Accelerate prototyping and development with Pinecone Local First-of-its-kind Pinecone Knowledge Platform to Power Best-in-class Retrieval for Customers Introducing integrated inference: Embed, rerank, and retrieve your data with a single API Strengthening security and increasing control with CMEK and API key roles Introducing Pinecone Rerank V0 Introducing cascading retrieval: Unifying dense and sparse with reranking From Idea to Action: How Pinecone Assistant Meaningfully Accelerates AI Business Building AI apps on Azure with Pinecone just got a lot easier Building a reliable, curated, and accurate RAG system with Cleanlab and Pinecone Four features of the Assistant API you aren't using - but should Deploying Pinecone with Infrastructure as Code (IaC) Streamlining CI/CD with Pinecone Local September 2024 Product Update Results of the Big ANN: NeurIPS'23 competition | Pinecone Introducing import from object storage for more efficient data transfer to Pinecone serverless Simplify, enhance, and evaluate RAG development with Pinecone Assistant, now in public preview Vectors and Graphs: Better Together August 2024 Product Update Pinecone Helps Deep Talk Deliver World-Class AI Assistants with Lower Engineering Overhead | Pinecone Assembled Delivers Better, Faster AI- Driven Support with Pinecone | Pinecone Llama 3.1 Agent using LangGraph and Ollama Build knowledgeable AI with Pinecone serverless, now generally available on Microsoft Azure Pinecone serverless is now generally available on Google Cloud, adding knowledge to AI assistants and other applications Accelerating Legal Discovery and Analysis with Pinecone and Voyage AI Bridging Dense and Sparse Maximum Inner Product Search | Pinecone Refine Retrieval Quality with Pinecone Rerank Introducing reranking to Pinecone Inference to simplify building accurate AI July 2024 Product Update Connect to Pinecone within your platform to enable a seamless AI development experience Introducing Pinecone API Versioning RAG Brag with Inkeep Co-Founder Nick Gomez LangGraph and Research Agents Introducing Pinecone Inference to streamline your AI workflow
Scaling AI Applications with Pinecone and Kubernetes - Ingestion Microservices
Roie Schwaber-Cohen · 2024-02-07 · via Pinecone

In this part, we’ll explore the architecture of the ingestion pipeline in more detail. We’ll dive into each one of the microservices and explain how they all work together. We’ll focus on the more significant subsets of the codebase, but the full source code is available if you want to dive even deeper.

Before diving in, let’s review the overall architecture again:

Overall architecture

In this part, we’ll focus on the ingestion services and how the work together to produce the searchable video application. As with many other AI systems, our pipeline includes some pre-processing steps that may be compute intensive. The AI capabilities themselves are also compute intensive, and so we need to take that into consideration when we architect the system to operate at scale.

In fact, the architectural goal of the pipeline as a whole is to allow the system to easily scale up as the payload size increases, such that the overall processing time doesn’t prohibitively increases. We accomplish this by doing two things:

  1. Breaking the original payload into smaller units of work.
  2. Parallelizing computation across multiple workers.

As previously discussed, it's important for our system to work properly that each worker is stateless. But why? One reason, as we've mentioned, is to ensure that multiple clients or consumers can experience consistent behavior from each worker.

Another reason is to ensure that if one worker fails, another can pick up the task without disrupting the overall process. This is crucial for maintaining high availability and fault tolerance in our system.

Finally, we want to simplify scaling. Stateless workers can be easily replicated across multiple machines as the workload increases, without worrying about maintaining a complex state across all of them.

While each worker may be stateless, we still need a way to persist data and move it around. For that purpose we use S3 as our distributed data store, and Kafka to send information about data we need to process. We don’t actually send the data itself over Kafka, but rather references to the stored data.

Kafka and Parallelization

Kafka is a distributed streaming platform designed to handle real-time data feeds with high-throughput and low-latency. It works on the principle of a publish-subscribe model, where producers generate data and send it to topics. Consumers, on the other hand, subscribe to these topics and process the data.

The power of Kafka lies in its ability to parallelize data processing. This is achieved through the concept of partitions within a topic. Each partition can be consumed by one consumer at a time, allowing for multiple consumers to read from a single topic in parallel. This way, if the number of partitions in a topic is equal to the number of consumers, each consumer can read from exactly one partition, making full use of its processing power.

In the context of our application, we use two Kafka topics:

  1. Frame extraction from video segments: Once our initial video is split into segment, each segment is sent to the video-segments topic. The frame extractor subscribes to that topic and then each instance of the frame extractor picks a video segment to extract frames from.
  2. Processing frames: When each frame is saved in S3, we send a message to the frames topic, and the subscribed Indexer consumer consumes the message. This allows us to parallelize the workload of object detection, embedding and upserting across multiple instances.

Throughout this post, we’ll see how Kafka is being used in practice.

Video Splitter

The video splitter downloads a video from a given URL, splits it into chunks, and uploads these chunks to an AWS S3 bucket. It uses the fluent-ffmpeg library to handle video processing tasks such as splitting the video into chunks.

Video splitter

  • Download Video: The downloadAndSplit function in downloadAndSplit.ts downloads a video from a given URL using the ytdl-core library. The video is saved locally as an MP4 file.
  • Split Video: the same downloadAndSplit function then splits the downloaded video into chunks using the fluent-ffmpg library. The duration of each chunk is specified by the chunkDuration parameter.
const split = async (
  videoPath: string,
  name: string,
  fps: number,
  chunkDuration: number,
  videoLimit: number,
): Promise<VideoOutput[]> => {
  const outputFolder = join(__dirname, `temp_files/${name}`)

  // Create the folder if it doesn't exist
  await fsPromises.mkdir(outputFolder, { recursive: true })
  const duration = await videoDuration(videoPath)

  const videoDurationsInSeconds = Math.min(
    duration, // Implement this function to get the video duration
    videoLimit,
  )

  const numberOfChunks = Math.ceil(videoDurationsInSeconds / chunkDuration)
  await log(`Splitting video into ${numberOfChunks} parts, ${videoDurationsInSeconds}, ${videoLimit}, ${duration}.`)
  const videoOutputs: VideoOutput[] = []

  // Start splitting the video using the segment option
  await new Promise<void>((resolve, reject) => {
    let count = 0
    ffmpeg(videoPath)
      .outputOptions([
        "-c copy", // Use stream copy mode to avoid re-encoding
        "-map 0", // Map all streams to the output
        `-segment_time ${chunkDuration}`, // Duration of each segment
        "-f segment", // Use the segment muxer
        "-reset_timestamps 1", // Reset timestamps at the beginning of each segment
        "-segment_start_number 0", // Start numbering segments from 0
        "-copyinkf", // Copy initial non-keyframes
      ])
      .output(join(outputFolder, "part_%d.mp4"))
      .on("progress", async () => {
        count += 1
        if (count % 10 === 0) {
          await log(`: ${count}`)
        }
        console.log(count)
      })
      .on("end", () => resolve())
      .on("error", (err) => reject(err))
      .run()
  })

  // Create video output information
  for (let i = 0; i < numberOfChunks; i += 1) {
    videoOutputs.push({
      videoPath: join(outputFolder, `part_${i}.mp4`),
      index: i,
    })
  }

  return videoOutputs
}
  • Upload to S3: Each video chunk is then uploaded to an AWS S3 bucket. The saveToS3Bucket function uses the @aws-sdk/client-s3 library in awsS3.ts is used for this purpose:
export const saveToS3Bucket = async (name: string, fileBuffer: Buffer) => {
	// Get S3 client
  const client = await getAwsS3Client() 
	// Define upload parameters
  const uploadParams = { 
    Bucket: AWS_S3_BUCKET,
    Key: name,
    Body: fileBuffer,
    ACL: "public-read",
  }

	// Upload the file to S3
  return client.send(new PutObjectCommand(uploadParams)) 
}
  • Kafka Messaging: After each video chunk is uploaded to S3, a message is sent to a Kafka topic with details about the video chunk. This is done in the sendMessage function of the KafkaProducer class in kafka-producer.ts. This is how the KafkaProducer is constructed:
constructor() {
    const kafka = new Kafka({
      clientId: "video-segments-producer-service",
      brokers: [`${KAFKA_BROKER}:9092`],
      logLevel: logLevel.INFO,
    })

    this.producer = kafka.producer({
      createPartitioner: Partitioners.DefaultPartitioner,
    })
    this.admin = kafka.admin()
    this.topic = "video-segments"
    this.isConnected = false
  }
  • When a new KafkaProducer object is created, it initializes a Kafka client with specific configurations such as client ID, brokers, and log level.
  • It also sets up a Kafka producer, which is responsible for sending messages to the Kafka broker, and a Kafka admin client, which is used for administrative tasks.
  • The constructor also sets the topic that this producer will be sending messages to, and initializes a connection status flag to false, indicating that the producer is not yet connected to the Kafka broker. We also create an admin instance which allows us to handle tasks like listing and creating topics.

The Kafka producer can send messages using the sendMessage function:

async sendMessage(message: string) {
    // Generate a random key
    const key = Math.random().toString(36).substring(2)
    try {
      // Check if the producer is connected before sending messages
      if (!this.isConnected) {
        console.log("It seems like the producer is not connected, trying to connect...")
        await this.connect()
      }
      const { topic } = this
      await log(`Sending message: ${message}`)
      await this.producer.send({
        topic: topic!,
        messages: [{
          value: message!,
          key,
        }],
      })
    } catch (error) {
      console.error("Error sending messages to Kafka:", error)
    }
  }
  • When called, sendMessages first generates a random key. This key is used to determine the partition that the message will be sent to in the Kafka topic. The randomness of the key is useful for load balancing: it will allow us to distribute the messages evenly across all partitions.
  • The method then checks if the producer is connected to the Kafka broker. If it's not, it attempts to establish a connection. Once the connection is confirmed, it logs the message being sent for debugging or auditing purposes.
  • The method then sends the message to the Kafka topic using the producer's send method. The message is sent with the previously generated key and the message value. If any errors occur during this process, such as issues with connecting to the broker or sending the message, they are caught and logged to the console.

Frame Extractor

The frame extractor behaves very similarly to the video splitter: it processes the video segments, extracts individual frames from each segment and uploads the frames to an AWS S3 bucket and saves some reference information about the frames in the Redis database. We will use this information later in the indexer and when we consume the frames in the application. It uses the fluent-ffmpeg library to extract frames and the @aws-sdk/client-s3 library to interact with S3.

Frame extractor

Here's a high-level overview of its main functionalities:

  • Extract Frames: The extractFrames function in processVideo.ts uses fluent-ffmpeg to extract frames from a video file. The frames are saved locally as PNG files.
const extractFrames = async (
  videoPath: string,
  name: string,
  index: string,
  fps: number,
  // eslint-disable-next-line no-async-promise-executor
): Promise<string[]> => new Promise(async (resolve, reject) => {
  let frameCount = 0
  const files: string[] = []
  const outputFolder = join(__dirname, `temp_files/${name}`)

  // Create the folder if it doesn't exist
  if (!existsSync(outputFolder)) {
    mkdirSync(outputFolder, { recursive: true })
  }

  // Use fluent-ffmpeg to extract frames
  ffmpeg(videoPath)
    .outputOptions([`-vf fps=${fps}`])
    .output(join(outputFolder, `${index}_%d.png`))
    .on("end", async () => {
			// Report total frames processed back to the main logger
      await log(`Total frames: ${frameCount}, ${index}`)
			// Iterate over each frame and save it in S3
      for (let i = 1; i <= frameCount; i += 1) {
        try {
          const outputFilePath = join(outputFolder, `${index}_${i}.png`)
          const fileBuffer = fs.readFileSync(outputFilePath)
					// Build a file path 
          const filePath = `${name}/frame/${index}_${i}.png`
					// Save the file in S3
          await saveToS3Bucket(filePath, fileBuffer)
          files.push(filePath)
          // Delete the local file
          await unlinkAsync(outputFilePath)
          // Send for indexing
          await log(`Sending message: ${filePath}}`)
          await trackFile(filePath)
					// Send a message to the Kafka queue with the file path. 
					// This kicks off the processing of each frame downstream
          await producer.sendMessage(filePath)
        } catch (e) {
          await log(`Error ${e}`)
          console.log(`ERROR ${e}`)
        }
      }
      await log("Frames extraction completed.")
      await log(`Extracted ${frameCount} frames.`)
      resolve(files)
    })
    .on("progress", async (progressData) => {
      frameCount = progressData.frames
      console.log(".")
      await log(`Frames extracted: ${frameCount}`)
    })
    .on("error", (error: Error) => {
      const err = `Error occurred: ${error.message}`
      reject(err)
    })
    .run()
})
  • Upload to S3: Each frame is then uploaded to an AWS S3 bucket. The saveToS3Bucket function in awsS3.ts is used for this purpose.
  • Kafka Messaging: After each frame is uploaded to S3, a message is sent to a Kafka topic with details about the frame. This is done in the sendMessage function of the KafkaProducer class in kafka-producer.ts.

Indexer

The indexer is the heart of the pipeline, and is responsible for processing images, detecting objects within them, embedding the identified objects and indexing them into a Pinecone index. It also saves each segment in S3 for later use in the application.

Indexer

  • Orchestration: The indexImages function is the main function that orchestrates the entire process. It fetches the frames stored in S3 then it processes each image and indexes the processed images into a Pinecone database. Additionally, it tracks the progress of the overall process and reports it back to the logger function in app-backend.
const indexImages = async ({ name, limit, filesList }: { name?: string; limit?: number; filesList?: string[]; }) => {
  // Get the S3 client
  const client = await getAwsS3Client()
  let list: string[] = []
  // If no filesList is provided and name is provided, get the list of files from S3
  if (!filesList && name) {
    const files = await client.send(
      new ListObjectsV2Command({
        Bucket: AWS_S3_BUCKET,
        Prefix: `${name}/frame`,
      }),
    )
    // If files are found, map them to a list
    if (files && files.Contents) {
      list = limit ? files.Contents.slice(0, limit).map((x) => x.Key!) : files.Contents.map((x) => x.Key!)
    }
  } else {
    // If filesList is not provided, throw an error
    if (!filesList) {
      throw new Error("No files list provided")
    }
    list = filesList
  }

  // Map each file in the list to a task
  const tasks = list.map(async (fileName) => {
    try {
      // Segment the image and filter out any undefined segments
      const segmentedFiles = (await segmentImage(fileName || ""))?.filter((x) => x) || []
      // Embed and upsert the segmented files
      await embedAndUpsert({ imagePaths: segmentedFiles, chunkSize: 100 })
      // Log the completion of indexing for the file
      await log(`Done indexing ${fileName}`)
      // Mark the file as complete
      await completeFile(fileName)
    } catch (error) {
      // Log any errors encountered during processing
      console.error(`Error processing file ${fileName}: ${error}`)
    }
  })
  // Wait for all tasks to complete
  await Promise.all(tasks)
}
  • Image retrieval segmentation: This functions retrieves the full URL where the frame is saved on S3, and then loads the raw image and passes it to the object detection function.
const fetchImageAndBoundingBoxes = async (imagePath: string) => {
  try {
    const url = generateS3Url(imagePath)
    const image = await RawImage.fromURL(url)
    const boundingBoxes = await detectObjects(image)
    return { image, boundingBoxes }
  } catch (error) {
    console.log(`Error in fetchImageAndBoundingBoxes: ${error}`)
    return undefined
  }
}
  • Object Detection: The detectObjects function uses the detr-resnet-50 model to detect objects within an image. It returns a list of bounding boxes for the detected objects.
const detectObjects = async (image: RawImage) => {
  try {
    const detector = await pipeline(
      "object-detection",
      "Xenova/detr-resnet-50",
    )
    // const image = await RawImage.read(url);
    const output = await detector(image, { threshold: 0.9 })
    return output
  } catch (e) {
    console.log(`Failed detecting object ${e}`)
    return false
  }
}
  • Processing the bounding boxes: This function uses the bounding box information to extract the images from the original frame, and then saves it in S3 and Redis.
const processBoundingBoxes = async (
  boundingBoxes: LabeledDetectedBoundingBox[],
  videoName: string,
  frameIndex: string,
  obj: Buffer,
) => {
  const files: FileWithReference[] = [] // Array to store processed bounding box files
  // Log the number of bounding boxes being processed
  await log(`Processing ${boundingBoxes.length} bounding boxes`, { boxesCount: boundingBoxes.length, eventType: "boxCount" })

  // Loop through each bounding box
  for (const element of boundingBoxes) {
		// Destructure box and label from the bounding box element
    const { box, label } = element
		// Generate a unique ID for the bounding box 
    const boxId = generateBoxId(box, frameIndex)
		// Convert the bounding box to the required format
    const boundingBox = convertBoundingBox(box)
		// Get metadata of the image
    const metadata = await sharp(obj).metadata()

    // Check if the bounding box is within the image boundaries
    if (
      boundingBox.left >= 0
      && boundingBox.top >= 0
      && boundingBox.left + boundingBox.width <= metadata.width!
      && boundingBox.top + boundingBox.height <= metadata.height!
    ) {
      // Extract the bounding box from the image and convert it to a buffer
      const bBoxBuffer = await sharp(obj).png().extract(boundingBox).toBuffer()

      // Define the path for the bounding box image in the S3 bucket
      const bboxPath = `${videoName}/bbox/${label}_${boxId}.png`
      // Save the bounding box image to the S3 bucket
      await saveToS3Bucket(bboxPath, bBoxBuffer)

      // Generate the URL for the bounding box image in the S3 bucket
      const bboxUrl = generateS3Url(bboxPath)

      // Check if the bounding box already exists in Redis
      if (!(await redis.hGet("bbox", boxId))) {
        // Define the bounding box object to be stored in Redis
        const bbox = {
          boxId,
          frameIndex,
          src: bboxUrl,
          boundingBox,
        }
        // Store the bounding box object in Redis
        await redis.hSet("bbox", boxId, JSON.stringify(bbox))
      }

      // Add the bounding box file to the files array
      files.push({
        boxId,
        path: bboxUrl,
        frameIndex: frameIndex.toString(),
      })
    }
  }
  // Return the array of processed bounding box files
  return files
}
  • Embedding Generation and Indexing: The embedAndUpsert function generates embeddings for the image segments using the the clip-vit-large-patch14 model and saves these embeddings into a Pinecone database.
async function embedAndUpsert({
  imagePaths,
  chunkSize,
}: {
  imagePaths: FileWithReference[];
  chunkSize: number;
}) {
  // Chunk the image paths into batches of size chunkSize
  const chunkGenerator = chunkArray(imagePaths, chunkSize)

  // Get the index
  const index = pineconeClient.index(PINECONE_INDEX)
  const ns = index.namespace(namespace)

  const embedders = []
  // Embed each batch and upsert the embeddings into the index
  for (const paths of chunkGenerator) {
    embedders.push(
      embedder.embedBatch(paths, chunkSize, async (embeddings) => {
        try {
          // Filter out any failed embeddings
          const filteredEmbeddings = embeddings.filter(
            (x) => x.values.length > 0,
          )
          await ns.upsert(filteredEmbeddings)
          log(`Done saving batch with ${filteredEmbeddings.length} embeddings}`, {
            eventType: "embeddingCount",
            embeddingCount: filteredEmbeddings.length,
          })
        } catch (e) {
          console.error(
            "error chunked upsert",
            e,
            embeddings.map((x) => x.id),
          )
        }
      }),
    )
  }
  // Run embedders in parallel and wait for them to finish
  await Promise.allSettled(embedders).then(console.log)
}

Similar to the frame extractor, we can create multiple instances of the indexer such each instance can process a portion of the produced frames - in parallel.

Centralized Logging

In order to maintain a cohesive picture of what the system is doing as a whole, we set logging endpoints in app-backend to track messages from the various services. To log arbitrary messages we is the log function:

const log = async (message: string, payload: object = {}) => {
  const podId = process.env.POD_NAME || "unknown"
  const formattedMessage = `${podId}: ${message}`
  try {
    await fetch(`http://${BACKEND}/log`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ message: formattedMessage, payload }),
    })
  } catch (error) {
    console.error("Failed to log message", error)
  }
}

To make it easier to track file completion, we use the trackFile method/endpoint which allows us to track the number of files processed in each service. In the next part of the series, we’ll take a look at how these metrics are communicated with the frontend.

Up next...

In this post, we reviewed into the architecture and functionality of the ingestion pipeline. We shed light on the main components - the video splitter, frame extractor, and indexer - elaborating on their roles within the system and their interaction with Kafka, S3, and Pinecone. This exploration highlights how the system maintains high availability, fault tolerance, and scalability while processing large volumes of data. In the next part, we’ll dive into the consumption side of the application. We’ll see how the front-end works, and how the data is queried.

Was this article helpful?