<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CocoIndex</title>
    <description>The latest articles on DEV Community by CocoIndex (cocoindex).</description>
    <link>https://dev.to/cocoindex</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F10462%2Ff476ad11-e7c2-4f0c-a5ff-99ba6e4011b4.png</url>
      <title>DEV Community: CocoIndex</title>
      <link>https://dev.to/cocoindex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cocoindex"/>
    <language>en</language>
    <item>
      <title>How I Built a Semantic Search Engine with CocoIndex</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Tue, 02 Dec 2025 22:04:22 +0000</pubDate>
      <link>https://dev.to/cocoindex/how-i-built-a-semantic-search-engine-with-cocoindex-5ak9</link>
      <guid>https://dev.to/cocoindex/how-i-built-a-semantic-search-engine-with-cocoindex-5ak9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this tutorial, I'll walk you through how I built a semantic search engine using &lt;strong&gt;CocoIndex&lt;/strong&gt;, an open-source Python library for creating powerful search experiences. If you've ever wanted to build a search feature that understands context and meaning (not just exact keyword matches), this post is for you!&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CocoIndex?
&lt;/h2&gt;

&lt;p&gt;CocoIndex is a lightweight semantic search library that makes it easy to index and search through documents using vector embeddings. Unlike traditional keyword-based search, semantic search understands the meaning behind queries, allowing users to find relevant results even when they use different words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose CocoIndex
&lt;/h2&gt;

&lt;p&gt;I needed a search solution that was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy to integrate&lt;/strong&gt; - No complex setup or infrastructure required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; - Quick indexing and search performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic&lt;/strong&gt; - Understanding context, not just keywords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source&lt;/strong&gt; - Free to use and modify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CocoIndex checked all these boxes!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;First, install CocoIndex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;cocoindex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the Search Engine
&lt;/h2&gt;

&lt;p&gt;Here's how I implemented the core functionality:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initialize CocoIndex
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CocoIndex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Add Documents
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cocoindex.flow_def(name="TextEmbedding")
def text_embedding_flow(flow_builder: cocoindex.FlowBuilder, data_scope: cocoindex.DataScope):
    """
    Define an example flow that embeds text into a vector database.
    """
    data_scope["documents"] = flow_builder.add_source(
        cocoindex.sources.LocalFile(path="markdown_files"))

    doc_embeddings = data_scope.add_collector()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Index the documents
&lt;/h1&gt;

&lt;p&gt;Process each document&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;with data_scope["documents"].row() as doc:
    doc["chunks"] = doc["content"].transform(
        cocoindex.functions.SplitRecursively(),
        language="markdown", chunk_size=2000, chunk_overlap=500)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;with doc["chunks"].row() as chunk:
    chunk["embedding"] = chunk["text"].transform(
        cocoindex.functions.SentenceTransformerEmbed(
            model="sentence-transformers/all-MiniLM-L6-v2"
        )
    )
    doc_embeddings.collect(filename=doc["filename"], location=chunk["location"],
                            text=chunk["text"], embedding=chunk["embedding"])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Export&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;doc_embeddings.export(
    "doc_embeddings",
    cocoindex.storages.Postgres(),
    primary_key_fields=["filename", "location"],
    vector_indexes=[
        cocoindex.VectorIndexDef(
            field_name="embedding",
            metric=cocoindex.VectorSimilarityMetric.COSINE_SIMILARITY)])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Perform Semantic Search
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ConnectionPool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_target_storage_default_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_embedding_flow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;query_vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_to_embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT filename, text, embedding &amp;lt;=&amp;gt; %s::vector AS distance
                FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ORDER BY distance LIMIT %s
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Features I Implemented
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fast Indexing
&lt;/h3&gt;

&lt;p&gt;CocoIndex uses efficient vector storage, making indexing thousands of documents quick and painless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic Understanding
&lt;/h3&gt;

&lt;p&gt;The search understands that "teaching computers" relates to "machine learning" even without exact keyword matches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizable Embeddings
&lt;/h3&gt;

&lt;p&gt;You can use different embedding models depending on your use case and accuracy requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example
&lt;/h2&gt;

&lt;p&gt;I built a documentation search for my project with 500+ markdown files. With CocoIndex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Indexing took less than 30 seconds&lt;/li&gt;
&lt;li&gt;Search response time averaged 50ms&lt;/li&gt;
&lt;li&gt;Users found relevant docs even with vague queries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Batch indexing&lt;/strong&gt; - Add multiple documents at once for better performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the right embedding model&lt;/strong&gt; - Balance between accuracy and speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache frequently accessed results&lt;/strong&gt; - Store common queries for instant responses&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Challenges I Faced
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Choosing Embedding Dimensions
&lt;/h3&gt;

&lt;p&gt;Higher dimensions = better accuracy but slower performance. I settled on 384 dimensions as a sweet spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Handling Large Document Collections
&lt;/h3&gt;

&lt;p&gt;For collections over 10k documents, I implemented pagination and lazy loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After implementing CocoIndex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User satisfaction increased significantly&lt;/li&gt;
&lt;li&gt;Implementation took only 2 days vs weeks for alternatives&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CocoIndex made building a semantic search engine surprisingly simple. Whether you're building a documentation site, blog search, or product catalog, it's a fantastic tool that punches above its weight.&lt;/p&gt;

&lt;p&gt;The library is actively maintained, well-documented, and the community is helpful. I highly recommend giving it a try for your next search implementation!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://cocoindex.io" rel="noopener noreferrer"&gt;Official Docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;My Demo Project: &lt;a href="https://cocoindex.io/docs/examples/simple_vector_index" rel="noopener noreferrer"&gt;Simple Vector Index Demo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you used CocoIndex or other semantic search libraries? Share your experience in the comments below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Happy coding! 🚀&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>Multimodal Face Recognition Pipeline with CocoIndex: Real-Time Image and Vector Search</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Tue, 29 Jul 2025 22:53:01 +0000</pubDate>
      <link>https://dev.to/cocoindex/multimodal-face-recognition-pipeline-with-cocoindex-real-time-image-and-vector-search-19hi</link>
      <guid>https://dev.to/cocoindex/multimodal-face-recognition-pipeline-with-cocoindex-real-time-image-and-vector-search-19hi</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/face-detection/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A folder of group photos is full of people, but a file index sees only filenames and bytes. Ask it for "every photo with this person in it" and it has nothing to compare.&lt;/p&gt;

&lt;p&gt;This post builds the index that can answer that question. It finds every face in every photo, turns each face into 128 numbers, and stores one &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; point per face, with the filename and the face's box. A face you search with then comes back with the photos it appears in and where it is in each one. The interactive demo on cocoindex.io uses the 1927 &lt;a href="https://en.wikipedia.org/wiki/Solvay_Conference" rel="noopener noreferrer"&gt;Solvay Conference&lt;/a&gt; photo, one of the 4 images that ship with the example. The boxes are the real output of the example's code.&lt;/p&gt;

&lt;p&gt;Detection and embedding come from the &lt;a href="https://github.com/ageitgey/face_recognition" rel="noopener noreferrer"&gt;&lt;code&gt;face_recognition&lt;/code&gt;&lt;/a&gt; library, which wraps &lt;a href="http://dlib.net/face_recognition.py.html" rel="noopener noreferrer"&gt;dlib&lt;/a&gt;. CocoIndex v1 runs the pipeline around it: it walks the image folder, runs each new or changed photo once, fans each photo out into one point per face, and removes a photo's points when the photo leaves the folder. The full code is in the &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/face_recognition" rel="noopener noreferrer"&gt;CocoIndex repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a face index lets you build
&lt;/h2&gt;

&lt;p&gt;Face embeddings identify people, and under laws such as the EU's GDPR (Article 9) and Illinois' BIPA they can count as biometric data. Index only photos you have the right to process this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image in, one point per face out
&lt;/h2&gt;

&lt;p&gt;A text pipeline splits a document into chunks. This pipeline splits a photo into faces, so 1 image becomes N points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Walk&lt;/strong&gt; &lt;code&gt;images/&lt;/code&gt; and match &lt;code&gt;.jpg&lt;/code&gt;, &lt;code&gt;.jpeg&lt;/code&gt; and &lt;code&gt;.png&lt;/code&gt; files. Each file becomes its own &lt;a href="https://cocoindex.io/docs/programming_guide/processing_component/" rel="noopener noreferrer"&gt;processing component&lt;/a&gt;, keyed by its path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect&lt;/strong&gt; every face with dlib's CNN detector, and crop each one out of the full-size photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed&lt;/strong&gt; each crop into a 128-float vector, and declare one Qdrant point per face. The point ID is a &lt;code&gt;uuid5&lt;/code&gt; of the filename and the box, and the payload carries both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write each step as a plain Python function, &lt;code&gt;target_state = transformation(source_state)&lt;/code&gt;. CocoIndex tracks which files changed, skips unchanged photos through &lt;a href="https://cocoindex.io/docs/programming_guide/function/#memoization" rel="noopener noreferrer"&gt;memoization&lt;/a&gt;, and keeps the Qdrant collection in step with the folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  One photo, end to end
&lt;/h2&gt;

&lt;p&gt;The stepper follows the Solvay photo through the 3 functions that do the work. Step with the buttons or the arrow keys.&lt;/p&gt;

&lt;p&gt;The rest of the post shows the code in the order it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/quick_start/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; on the default ports: &lt;code&gt;docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Python 3.11 or later. &lt;code&gt;face_recognition&lt;/code&gt; needs dlib, which builds from source on some platforms; its &lt;a href="https://github.com/ageitgey/face_recognition#installation" rel="noopener noreferrer"&gt;installation notes&lt;/a&gt; cover CMake and the other build tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CocoIndex v1 keeps its own processing state in a local directory set by &lt;code&gt;COCOINDEX_DB&lt;/code&gt; (the example's &lt;code&gt;.env.example&lt;/code&gt; sets &lt;code&gt;./cocoindex.db&lt;/code&gt;), so you do not run a separate database for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the pipeline
&lt;/h2&gt;

&lt;p&gt;Everything is in &lt;a href="https://github.com/cocoindex-io/cocoindex/blob/main/examples/face_recognition/main.py" rel="noopener noreferrer"&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/a&gt;. It starts with the imports, the constants and the Qdrant client's context key:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
from &lt;strong&gt;future&lt;/strong&gt; import annotations&lt;/p&gt;

&lt;p&gt;import io&lt;br&gt;
import os&lt;br&gt;
import pathlib&lt;br&gt;
import sys&lt;br&gt;
import uuid&lt;br&gt;
from dataclasses import dataclass&lt;br&gt;
from typing import Any, AsyncIterator&lt;/p&gt;

&lt;p&gt;import face_recognition&lt;br&gt;
import numpy as np&lt;br&gt;
from PIL import Image&lt;br&gt;
from qdrant_client import QdrantClient&lt;/p&gt;

&lt;p&gt;import cocoindex as coco&lt;br&gt;
from cocoindex.connectors import localfs, qdrant&lt;br&gt;
from cocoindex.resources.file import FileLike, PatternFilePathMatcher&lt;br&gt;
from cocoindex.resources.schema import VectorSchema&lt;/p&gt;

&lt;p&gt;QDRANT_COLLECTION = "face_embeddings"&lt;br&gt;
FACE_EMBED_DIM = 128  # face_recognition (dlib) face encodings are 128-d&lt;br&gt;
MAX_IMAGE_WIDTH = 1280  # the CNN detector is slow on large images; downscale first&lt;br&gt;
TOP_K = 5&lt;/p&gt;

&lt;p&gt;def qdrant_url() -&amp;gt; str:&lt;br&gt;
    return os.getenv("QDRANT_URL", "&lt;a href="http://localhost:6334/%22" rel="noopener noreferrer"&gt;http://localhost:6334/"&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;QDRANT_DB = coco.ContextKey&lt;a href="https://dev.to%22face_qdrant%22"&gt;QdrantClient&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`MAX_IMAGE_WIDTH` bounds the cost of detection: the CNN detector is slow on large images, so wider photos are shrunk for it. A face is a box plus the cropped pixels:



```python title=main.py
@dataclass
class ImageRect:
    min_x: int
    min_y: int
    max_x: int
    max_y: int

@dataclass
class Face:
    """One detected face: its bounding box and the cropped PNG."""

    rect: ImageRect
    image: bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Detect and crop the faces
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;extract_faces&lt;/code&gt; runs the detector on a copy that is at most 1280 pixels wide, then multiplies each box by &lt;code&gt;ratio&lt;/code&gt; so it lands on the original photo. The crop comes from the original, so the embedding sees every pixel of the face:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn.as_async(runner=coco.GPU)&lt;br&gt;
def extract_faces(content: bytes) -&amp;gt; list[Face]:&lt;br&gt;
    """Detect faces in an image and return each as a (bounding box, cropped PNG)."""&lt;br&gt;
    orig = Image.open(io.BytesIO(content)).convert("RGB")&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Downscale large images for the detector, then map boxes back to full size.
if orig.width &amp;gt; MAX_IMAGE_WIDTH:
    ratio = orig.width / MAX_IMAGE_WIDTH
    small = orig.resize(
        (MAX_IMAGE_WIDTH, int(orig.height / ratio)),
        resample=Image.Resampling.BICUBIC,
    )
else:
    ratio = 1.0
    small = orig

faces: list[Face] = []
for top, right, bottom, left in face_recognition.face_locations(
    np.array(small), model="cnn"
):
    rect = ImageRect(
        min_x=int(left * ratio),
        min_y=int(top * ratio),
        max_x=int(right * ratio),
        max_y=int(bottom * ratio),
    )
    buf = io.BytesIO()
    orig.crop((rect.min_x, rect.min_y, rect.max_x, rect.max_y)).save(
        buf, format="PNG"
    )
    faces.append(Face(rect=rect, image=buf.getvalue()))

return faces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`face_locations` returns boxes as `(top, right, bottom, left)`; `ImageRect` stores them as `min_x`, `min_y`, `max_x`, `max_y`. On the Solvay photo (2560 × 1853), the detector sees 1280 × 926 and finds 29 faces.

### Embed one face

`embed_face` passes the whole crop to dlib's encoder as the face location and returns 128 floats:



```python title=main.py
@coco.fn.as_async(runner=coco.GPU)
def embed_face(face_png: bytes) -&amp;gt; list[float]:
    """Embed a single cropped face into a 128-d vector."""
    img = Image.open(io.BytesIO(face_png)).convert("RGB")
    arr = np.array(img)
    encoding = face_recognition.face_encodings(
        arr, known_face_locations=[(0, img.width - 1, img.height - 1, 0)]
    )[0]
    return encoding.tolist()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both functions are synchronous dlib calls, so they are declared with &lt;a href="https://cocoindex.io/docs/programming_guide/function/#runner" rel="noopener noreferrer"&gt;&lt;code&gt;@coco.fn.as_async(runner=coco.GPU)&lt;/code&gt;&lt;/a&gt;. Callers await them, and the GPU runner runs them in a worker thread, one call at a time per GPU it finds (a machine without a GPU counts as 1).&lt;/p&gt;
&lt;h3&gt;
  
  
  One point per face
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;process_face&lt;/code&gt; embeds a face and declares its point. The ID comes from &lt;code&gt;_face_id&lt;/code&gt;, a &lt;code&gt;uuid5&lt;/code&gt; of the filename and the box, so the same box in the same file always maps to the same point:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
def _face_id(filename: str, rect: ImageRect) -&amp;gt; str:&lt;br&gt;
    key = f"{filename}|{rect.min_x},{rect.min_y},{rect.max_x},{rect.max_y}"&lt;br&gt;
    return str(uuid.uuid5(uuid.NAMESPACE_URL, key))&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def process_face(&lt;br&gt;
    face: Face,&lt;br&gt;
    filename: str,&lt;br&gt;
    target: qdrant.CollectionTarget,&lt;br&gt;
) -&amp;gt; None:&lt;br&gt;
    """Embed one face and declare it as a Qdrant point."""&lt;br&gt;
    embedding = await embed_face(face.image)&lt;br&gt;
    target.declare_point(&lt;br&gt;
        qdrant.PointStruct(&lt;br&gt;
            id=_face_id(filename, face.rect),&lt;br&gt;
            vector=embedding,&lt;br&gt;
            payload={&lt;br&gt;
                "filename": filename,&lt;br&gt;
                "min_x": face.rect.min_x,&lt;br&gt;
                "min_y": face.rect.min_y,&lt;br&gt;
                "max_x": face.rect.max_x,&lt;br&gt;
                "max_y": face.rect.max_y,&lt;br&gt;
            },&lt;br&gt;
        )&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`declare_point` does not write anything itself. It states which point should exist, and CocoIndex creates, updates or deletes points to match after the component finishes (see [target states](https://cocoindex.io/docs/programming_guide/target_state/)).

### One component per photo

`process_file` handles one image: it detects the faces, then runs `process_face` for each with [`coco.map`](https://cocoindex.io/docs/programming_guide/processing_component/#using-cocoindex-map-to-apply-transformations):



```python title=main.py
@coco.fn(memo=True)
async def process_file(file: FileLike, target: qdrant.CollectionTarget) -&amp;gt; None:
    """Detect every face in one image and index each one."""
    faces = await extract_faces(await file.read())
    filename = str(file.file_path.path)
    await coco.map(process_face, faces, filename, target)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;coco.map&lt;/code&gt;&lt;/strong&gt; runs the calls concurrently inside the current component. It creates no child components, so all the points of one photo belong to that photo's component, and they are synced together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memo=True&lt;/code&gt;&lt;/strong&gt; memoizes the whole component. While a photo and the code are unchanged, the photo skips detection and embedding, and its points are carried over as they are.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Wire up the app
&lt;/h3&gt;

&lt;p&gt;The lifespan creates one Qdrant client for the app, and &lt;code&gt;app_main&lt;/code&gt; declares the collection and mounts one &lt;code&gt;process_file&lt;/code&gt; per image:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.lifespan&lt;br&gt;
async def coco_lifespan(builder: coco.EnvironmentBuilder) -&amp;gt; AsyncIterator[None]:&lt;br&gt;
    builder.provide(QDRANT_DB, qdrant.create_client(qdrant_url(), prefer_grpc=True))&lt;br&gt;
    yield&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def app_main(sourcedir: pathlib.Path) -&amp;gt; None:&lt;br&gt;
    target_collection = await qdrant.mount_collection_target(&lt;br&gt;
        QDRANT_DB,&lt;br&gt;
        collection_name=QDRANT_COLLECTION,&lt;br&gt;
        schema=await qdrant.CollectionSchema.create(&lt;br&gt;
            vectors=qdrant.QdrantVectorDef(&lt;br&gt;
                schema=VectorSchema(dtype=np.dtype(np.float32), size=FACE_EMBED_DIM),&lt;br&gt;
                distance="euclid",  # dlib face encodings compare by Euclidean distance&lt;br&gt;
            )&lt;br&gt;
        ),&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files = localfs.walk_dir(
    sourcedir,
    recursive=True,
    path_matcher=PatternFilePathMatcher(
        included_patterns=["**/*.jpg", "**/*.jpeg", "**/*.png"]
    ),
    live=True,  # api supports live watch; pass -L to `cocoindex update` to run live
)
await coco.mount_each(process_file, files.items(), target_collection)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;app = coco.App(&lt;br&gt;
    coco.AppConfig(name="FaceRecognitionV1"),&lt;br&gt;
    app_main,&lt;br&gt;
    sourcedir=pathlib.Path("./images"),&lt;br&gt;
)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


- **`distance="euclid"`**: dlib's encodings are compared by Euclidean distance. `face_recognition` treats 2 faces at a distance of 0.6 or less as the same person by default (the `tolerance` of `compare_faces`).
- **`files.items()`** keys each file by its path relative to `images/`. That key names the file's component, so CocoIndex can tell which files are new, changed or gone.
- **`live=True`** lets the source keep watching the folder when the app runs in live mode (`cocoindex update -L main`). A plain `cocoindex update main` processes what changed and exits.

## Run it

From a clone of the [CocoIndex repo](https://github.com/cocoindex-io/cocoindex):



```sh
cd cocoindex/examples/face_recognition
docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant
cp .env.example .env     # QDRANT_URL (defaults to the local container above)
pip install -e .
cocoindex update main    # or: cocoindex update -L main  (keep watching the folder)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The example ships 4 photos in &lt;code&gt;images/&lt;/code&gt;. The first run finds 36 faces: 29 in the Solvay photo, 3 in &lt;code&gt;Carter_welcomes_Reagan.jpg&lt;/code&gt;, and 2 each in the Steve Jobs and Bill Gates photo and in &lt;code&gt;einplanck3.jpg&lt;/code&gt;. Each face is one point in the &lt;code&gt;face_embeddings&lt;/code&gt; collection.&lt;/p&gt;
&lt;h2&gt;
  
  
  What an edit to the folder costs
&lt;/h2&gt;

&lt;p&gt;Run the update again after a change, and CocoIndex runs only the photos that changed. Each button below is one edit followed by &lt;code&gt;cocoindex update main&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;What decides each outcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New photo.&lt;/strong&gt; A new path mounts a new component with no memo entry, so &lt;code&gt;extract_faces&lt;/code&gt; runs once and &lt;code&gt;embed_face&lt;/code&gt; runs once per face.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same bytes, new modification time.&lt;/strong&gt; The memo state of a file is its modification time plus a fingerprint of its bytes. The time changed, so CocoIndex fingerprints the bytes, finds them unchanged, and skips the component. Its points stay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New bytes.&lt;/strong&gt; The component runs again: one detection, and one embedding for every face in the photo, including faces that did not change, because neither &lt;code&gt;process_face&lt;/code&gt; nor &lt;code&gt;embed_face&lt;/code&gt; is memoized. Cropping the left or top edge shifts every box, so every face gets a new ID; the new points are upserted and the old ones, no longer declared, are deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename.&lt;/strong&gt; The old path's component is gone, so its points are deleted. The new path has no memo entry and runs in full, and since the filename is part of the ID, its points get new IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete.&lt;/strong&gt; The component is removed, and CocoIndex deletes all of its points.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A change to the logic of &lt;code&gt;process_file&lt;/code&gt;, or of the &lt;code&gt;@coco.fn&lt;/code&gt; functions it calls (&lt;code&gt;extract_faces&lt;/code&gt;, &lt;code&gt;process_face&lt;/code&gt;, &lt;code&gt;embed_face&lt;/code&gt;), invalidates the memo too, and the next update re-runs every photo. Comments and formatting do not count.&lt;/p&gt;
&lt;h2&gt;
  
  
  Search by face
&lt;/h2&gt;

&lt;p&gt;The original version of this post sketched the goal: a face found in one photo, looked up in another.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main.py&lt;/code&gt; also has a small query command. It detects the faces in a query image, embeds the first one it finds with the same dlib encoder (inside the whole photo rather than as a crop), and asks Qdrant for the 5 nearest points:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
def _qdrant_search(&lt;br&gt;
    client: QdrantClient, query_vector: list[float], limit: int&lt;br&gt;
) -&amp;gt; list[Any]:&lt;br&gt;
    return client.query_points(&lt;br&gt;
        collection_name=QDRANT_COLLECTION,&lt;br&gt;
        query=query_vector,&lt;br&gt;
        limit=limit,&lt;br&gt;
        with_payload=True,&lt;br&gt;
    ).points&lt;/p&gt;

&lt;p&gt;def query(image_path: str, *, top_k: int = TOP_K) -&amp;gt; None:&lt;br&gt;
    content = pathlib.Path(image_path).read_bytes()&lt;br&gt;
    img = Image.open(io.BytesIO(content)).convert("RGB")&lt;br&gt;
    arr = np.array(img)&lt;br&gt;
    locs = face_recognition.face_locations(arr, model="cnn")&lt;br&gt;
    if not locs:&lt;br&gt;
        print("No face found in the query image.")&lt;br&gt;
        return&lt;br&gt;
    query_vec = face_recognition.face_encodings(arr, known_face_locations=locs[:1])[&lt;br&gt;
        0&lt;br&gt;
    ].tolist()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client = qdrant.create_client(qdrant_url(), prefer_grpc=True)
for r in _qdrant_search(client, query_vec, top_k):
    payload = r.payload or {}
    # Euclidean distance: smaller is more similar (same person typically &amp;lt; 0.6).
    print(f"[{r.score:.3f}] {payload.get('filename')}  {payload}")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;




```sh
python main.py query images/einplanck3.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;einplanck3.jpg&lt;/code&gt; shows Max Planck and Albert Einstein, who both sit in the front row of the Solvay photo. The interactive demo on cocoindex.io runs the query for each of them against the 36 indexed faces.&lt;/p&gt;

&lt;p&gt;The results show how to use this kind of index:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The photo's own crop comes first.&lt;/strong&gt; &lt;code&gt;einplanck3.jpg&lt;/code&gt; is in the index too, and its stored crop of the query face is 0.230 away for Planck and 0.187 for Einstein, not 0, because the query encodes the face inside the whole photo and the index encodes the tight crop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, old faces crowd together.&lt;/strong&gt; Most Solvay faces are about 80 pixels wide in a 1927 scan. Planck's face in the Solvay photo is 0.472 away, 0.009 behind a different man. Einstein's is 0.587 away, under 0.6, but 11 points are closer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranked results are candidates.&lt;/strong&gt; Show the top hits with their boxes and let a person confirm, or require a tighter distance before you treat a hit as a match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To go further, search with every face in a photo instead of the first one, filter the hits by &lt;code&gt;filename&lt;/code&gt; in the payload, or draw each hit's box on its photo.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cocoindex.io/docs/examples/face-recognition/" rel="noopener noreferrer"&gt;face recognition walkthrough&lt;/a&gt; in the docs covers the same example, and the &lt;a href="https://cocoindex.io/blogs/live-image-search" rel="noopener noreferrer"&gt;image search post&lt;/a&gt; shows a query API and a live index inside a FastAPI server.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>showdev</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build Real-Time Codebase Indexing for AI Coding agents</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Sun, 13 Jul 2025 08:10:15 +0000</pubDate>
      <link>https://dev.to/cocoindex/build-real-time-codebase-indexing-for-ai-coding-agents-5eb2</link>
      <guid>https://dev.to/cocoindex/build-real-time-codebase-indexing-for-ai-coding-agents-5eb2</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/index-codebase-v1/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Build a live semantic index over a codebase with &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex V1&lt;/a&gt;. Point it at a repo, and you get a vector index you can search in natural language ("where do we embed chunks?") that updates itself as you edit. It's the kind of fresh, low-latency code context a &lt;a href="https://cocoindex.io/docs/getting_started/ai_coding_agents/" rel="noopener noreferrer"&gt;coding agent&lt;/a&gt; needs, and it's about 100 lines of plain async Python.&lt;/p&gt;

&lt;p&gt;CocoIndex has built-in support for codebase chunking, with native Tree-sitter parsing and live updates.&lt;/p&gt;

&lt;p&gt;The heavy lifting (&lt;a href="https://cocoindex.io/blogs/incremental-processing" rel="noopener noreferrer"&gt;incremental processing&lt;/a&gt;, change tracking, managed targets) runs in a Rust engine underneath. With a source that watches for changes, like the local filesystem in &lt;code&gt;live&lt;/code&gt; mode, the index tracks your edits as they happen: save a file, and only the chunks that changed get re-embedded and re-upserted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code context for agents: semantic context for Claude, Codex, OpenCode, Factory.&lt;/li&gt;
&lt;li&gt;Code search: natural-language and semantic search over your repo.&lt;/li&gt;
&lt;li&gt;Review &amp;amp; refactor agents: context for code review, security analysis, and large-scale refactoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why CocoIndex for codebase indexing
&lt;/h2&gt;

&lt;p&gt;A codebase is hard to keep indexed well, and it exercises most of what CocoIndex was built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chunks follow real code structure (functions, classes, blocks) instead of arbitrary line windows. Tree-sitter parsing is built in for every major language.&lt;/li&gt;
&lt;li&gt;Code changes constantly: every commit, save, and rebase mutates a handful of files. CocoIndex re-embeds only the chunks that changed, and with &lt;code&gt;live=True&lt;/code&gt; plus &lt;code&gt;cocoindex update -L&lt;/code&gt; it keeps watching and applies each edit as it happens.&lt;/li&gt;
&lt;li&gt;The engine is Rust. The run below indexes 622 files in 43 seconds cold; after that, a save costs one file.&lt;/li&gt;
&lt;li&gt;The pipeline is ordinary &lt;code&gt;async&lt;/code&gt; Python and your own types. Pick your embedding model, chunking strategy, and vector database; if your agent can write Python, it can extend this flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
  href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/code_embedding"&lt;br&gt;
  path="examples/code_embedding"&lt;br&gt;
  description="The whole example is ~100 lines of plain Python. Clone it, point it at your repo, and swap in your favorite model or vector store."&lt;br&gt;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;From a high level, these are the steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read code files from a local directory.&lt;/li&gt;
&lt;li&gt;Split each file into syntax-aware chunks with Tree-sitter, then embed every chunk.&lt;/li&gt;
&lt;li&gt;Store the chunks and their embeddings in Postgres (as &lt;a href="https://cocoindex.io/docs/programming_guide/target_state/" rel="noopener noreferrer"&gt;&lt;strong&gt;target states&lt;/strong&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With CocoIndex, you &lt;a href="https://cocoindex.io/docs/programming_guide/core_concepts/" rel="noopener noreferrer"&gt;declare the transformation logic&lt;/a&gt; with native Python, without worrying about handling updates. &lt;/p&gt;

&lt;p&gt;Think: &lt;strong&gt;target_state = transformation(source_state)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When your codebase changes, or your processing logic changes (a different embedding model, a new chunk size), only the difference is reprocessed. The Incremental updates section walks through exactly what gets recomputed when.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;In this example we store the indexed rows in Postgres with the &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; extension. Your metadata and the vector search index both live there. CocoIndex supports &lt;a href="https://cocoindex.io/docs/connectors/postgres/" rel="noopener noreferrer"&gt;many targets&lt;/a&gt;, so you can pick the one that works best for you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you don't have Postgres running, use the Docker Compose file in the &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;cocoindex repo&lt;/a&gt; to start one.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; dev/postgres.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Point CocoIndex at it
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgres://cocoindex:cocoindex@localhost/cocoindex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Install CocoIndex and the dependencies
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"cocoindex[postgres,sentence_transformers]"&lt;/span&gt; asyncpg pgvector numpy python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Define the App
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cocoindex.io/docs/programming_guide/app/" rel="noopener noreferrer"&gt;Apps&lt;/a&gt; are the top-level runnable unit in CocoIndex. An app names your pipeline and binds a main function with its parameters.&lt;/p&gt;
&lt;h3&gt;
  
  
  Define the data and shared resources
&lt;/h3&gt;

&lt;p&gt;This block sets up two things the rest of the code builds on. &lt;code&gt;CodeEmbedding&lt;/code&gt; defines one row of the output table: each chunk of code becomes one row, with its text, location, and embedding vector. &lt;code&gt;coco_lifespan&lt;/code&gt; provides the &lt;a href="https://cocoindex.io/docs/programming_guide/context/" rel="noopener noreferrer"&gt;shared resources&lt;/a&gt; every step needs, the Postgres connection pool and the embedding model, built once at startup instead of per file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncIterator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncpg&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;numpy.typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NDArray&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cocoindex&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex.connectors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;localfs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex.ops.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RecursiveSplitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detect_code_language&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex.ops.sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformerEmbedder&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex.resources.chunk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chunk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex.resources.file&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileLike&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PatternFilePathMatcher&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cocoindex.resources.id&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IdGenerator&lt;/span&gt;

&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://cocoindex:cocoindex@localhost/cocoindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;PG_DB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContextKey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncpg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_embedding_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;EMBEDDER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContextKey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SentenceTransformerEmbedder&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detect_change&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecursiveSplitter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CodeEmbedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NDArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EMBEDDER&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;start_line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;end_line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="nd"&gt;@coco.lifespan&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;coco_lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnvironmentBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncIterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncpg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PG_DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMBEDDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformerEmbedder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMBED_MODEL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Define file processing
&lt;/h3&gt;

&lt;p&gt;With the row schema and shared resources in place, we can write the actual work: turning a file into rows. &lt;/p&gt;

&lt;h4&gt;
  
  
  Process file
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@coco.fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FileLike&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableTarget&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CodeEmbedding&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detect_code_language&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_splitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;min_chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;id_gen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IdGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_gen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;process_file&lt;/code&gt; runs once per file. It reads the file, detects the language so Tree-sitter can parse it, &lt;a href="https://cocoindex.io/docs/ops/text/" rel="noopener noreferrer"&gt;splits the code&lt;/a&gt; along the syntax tree, and maps each chunk to &lt;code&gt;process_chunk&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Each chunk is a coherent syntactic unit, so retrieval returns whole functions or blocks, never a fragment cut mid-statement. All major languages are supported (Python, Rust, JavaScript, TypeScript, Java, C++, and more); unknown types fall back to plain text.&lt;/p&gt;

&lt;p&gt;Note that &lt;a href="https://cocoindex.io/docs/programming_guide/function/" rel="noopener noreferrer"&gt;&lt;code&gt;@coco.fn&lt;/code&gt;&lt;/a&gt; with &lt;a href="https://cocoindex.io/docs/advanced_topics/memoization_keys/" rel="noopener noreferrer"&gt;&lt;code&gt;memo=True&lt;/code&gt;&lt;/a&gt; is what makes this incremental: if a file's content and this function's code are both unchanged, the whole file is skipped on the next run. &lt;code&gt;coco.map&lt;/code&gt; fans out to one &lt;code&gt;process_chunk&lt;/code&gt; call per chunk.&lt;/p&gt;

&lt;p&gt;Here is what chunking produces: each file is split into syntactic chunks, each with its own location and text.&lt;/p&gt;

&lt;h4&gt;
  
  
  Process chunk
&lt;/h4&gt;

&lt;p&gt;Next, we embed the chunk with the shared embedder and declare the target row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@coco.fn&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PurePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;id_gen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IdGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableTarget&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CodeEmbedding&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EMBEDDER&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;declare_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;CodeEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;id_gen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;start_line&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;end_line&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use &lt;a href="https://cocoindex.io/docs/ops/sentence_transformers/" rel="noopener noreferrer"&gt;&lt;code&gt;SentenceTransformerEmbedder&lt;/code&gt;&lt;/a&gt; with &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;; there are 12k+ sentence-transformer models on &lt;a href="https://huggingface.co/models?other=sentence-transformers" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;, so swap in whichever you prefer. Note &lt;code&gt;chunk.start.line&lt;/code&gt; and &lt;code&gt;chunk.end.line&lt;/code&gt;: V1 carries line numbers through, so search results point straight at the lines that matched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the main function
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;app_main&lt;/code&gt; wires the source to the target. It mounts the Postgres table (with a &lt;a href="https://cocoindex.io/docs/common_resources/vector_schema/" rel="noopener noreferrer"&gt;vector index&lt;/a&gt;), walks the codebase, and mounts one &lt;a href="https://cocoindex.io/docs/programming_guide/processing_component/" rel="noopener noreferrer"&gt;processing component&lt;/a&gt; per file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@coco.fn&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourcedir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;target_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_table_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;PG_DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TableSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;CodeEmbedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;target_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;declare_vector_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localfs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;sourcedir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;path_matcher&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PatternFilePathMatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;included_patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.rs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.toml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.mdx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;excluded_patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/.*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/node_modules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# watch for changes; pass -L to `cocoindex update` to run live
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;target_table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We walk the codebase from &lt;code&gt;sourcedir&lt;/code&gt; (point this at the repository you want to index). We index files with the extensions &lt;code&gt;.py&lt;/code&gt;, &lt;code&gt;.rs&lt;/code&gt;, &lt;code&gt;.toml&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt;, &lt;code&gt;.mdx&lt;/code&gt;, and skip dot-directories, &lt;code&gt;target&lt;/code&gt;, and &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mount_table_target&lt;/code&gt; creates and manages the Postgres table for you: schema, the pgvector index, idempotent upserts, and orphan cleanup when a file disappears. &lt;code&gt;live=True&lt;/code&gt; makes the &lt;a href="https://cocoindex.io/docs/connectors/localfs/" rel="noopener noreferrer"&gt;filesystem source&lt;/a&gt; &lt;a href="https://cocoindex.io/docs/programming_guide/live_mode/" rel="noopener noreferrer"&gt;watch for changes&lt;/a&gt;, and &lt;code&gt;mount_each&lt;/code&gt; runs one component per file so the engine can track and update them independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the App
&lt;/h3&gt;

&lt;p&gt;Bind &lt;code&gt;app_main&lt;/code&gt; into a &lt;code&gt;coco.App&lt;/code&gt; and point it at the codebase root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;coco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CodeEmbeddingV1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sourcedir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# index from repo root
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire indexing path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the pipeline
&lt;/h2&gt;

&lt;p&gt;Run the &lt;a href="https://cocoindex.io/docs/cli/" rel="noopener noreferrer"&gt;&lt;code&gt;cocoindex&lt;/code&gt; CLI&lt;/a&gt; to set up and update the index. Choose catch-up (scan, sync, exit) or live (catch up, then keep watching):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Catch-up run&lt;/span&gt;
cocoindex update main

&lt;span class="c"&gt;# Live run: keep watching for file changes&lt;/span&gt;
cocoindex update &lt;span class="nt"&gt;-L&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the index update state in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ app_main: 1 total | 1 added
✅ declare_target_state_with_child: 1 total | 1 added
✅ _MountEachLiveComponent.process: 1 total | 1 added
✅ process_file: 622 total | 622 added
⏳ Elapsed: 43.4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Query the index
&lt;/h2&gt;

&lt;p&gt;We match user text against the index with a plain SQL query, reusing the &lt;em&gt;same&lt;/em&gt; embedder from the indexing flow so indexing and querying stay consistent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;query_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            SELECT filename, code, embedding &amp;lt;=&amp;gt; $1 AS distance, start_line, end_line
            FROM &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
            ORDER BY distance ASC
            LIMIT $2
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;query_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (L&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;start_line&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-L&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;end_line&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; operator is pgvector's cosine distance. We turn it into a similarity score and print the filename, the matched line range, and the code snippet. Run it against your index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="s2"&gt;"where do we embed chunks"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints the top five chunks, each with its similarity score, file, and line range. Snippets are cut to their first lines here (&lt;code&gt;…&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.527] rust/ops_text/src/split/by_separators.rs (L72-L94)
            let bytes = text.as_bytes();

        // Collect raw chunks (byte ranges)
        struct RawChunk {
            start: usize,
            end: usize,
    …
---
[0.459] rust/ops_text/src/split/by_separators.rs (L116-L138)
            let mut positions: Vec&amp;lt;Position&amp;gt; = raw_chunks
            .iter()
            .flat_map(|c| vec![Position::new(c.start), Position::new(c.end)])
            .collect();

        set_output_positions(text, positions.iter_mut());
    …
---
[0.448] rust/ops_text/src/split/recursive.rs (L560-L578)
        fn split_root_chunk(&amp;amp;self, kind: ChunkKind&amp;lt;'t&amp;gt;) -&amp;gt; Vec&amp;lt;ChunkOutput&amp;gt; {
        let mut atom_collector = AtomChunksCollector {
            full_text: self.full_text,
            min_level: 0,
            curr_level: 0,
            atom_chunks: Vec::new(),
    …
---
[0.447] rust/sdk/cocoindex/tests/code.rs (L300-L314)
    #[test]
fn code_ast_split_chunk_positions_are_accurate() {
    // Three-line Python source; verify start/end positions of first chunk.
    let src = "a = 1\nb = 2\nc = 3\n";
    let ast = CodeAst::new(src, "python").unwrap();
    let chunks = ast.split(200, None, None).unwrap(); // one big chunk
    …
---
[0.444] rust/sdk/cocoindex/src/ops/code.rs (L315-L341)
            let config = RecursiveChunkConfig {
            chunk_size,
            min_chunk_size,
            chunk_overlap,
        };
        let raw = chunker.split(&amp;amp;self.source, config);
    …
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Incremental updates
&lt;/h2&gt;

&lt;p&gt;You never compute a diff or write update logic: you change something, and CocoIndex works out what to embed, upsert, and delete. &lt;code&gt;@coco.fn(memo=True)&lt;/code&gt; decides what to &lt;em&gt;recompute&lt;/em&gt;: a file is skipped when its content and the function's code are both unchanged, and an embedding is reused when its chunk text is unchanged. &lt;code&gt;mount_table_target&lt;/code&gt; decides what to &lt;em&gt;write&lt;/em&gt;: each row's &lt;code&gt;id&lt;/code&gt; is derived from its chunk's content, so only rows that actually changed are upserted, and rows whose source is gone are deleted.&lt;/p&gt;

&lt;p&gt;The same machinery covers two kinds of change: changes to your &lt;strong&gt;data&lt;/strong&gt; (the code being indexed) and changes to your &lt;strong&gt;logic&lt;/strong&gt; (the pipeline itself).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data changes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A file is added&lt;/strong&gt;: only that file is chunked and embedded, and its rows are inserted. The rest of the repo is untouched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A file is deleted&lt;/strong&gt;: its rows are removed from the target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A file is edited&lt;/strong&gt;: the file is re-chunked, and the new chunks usually overlap the old ones. Chunks whose text is unchanged keep their &lt;code&gt;id&lt;/code&gt; and embedding, so they are left as-is; genuinely new chunks are embedded and inserted; chunks that no longer exist are deleted. Edit one function and only that function's chunks are re-embedded, even though the whole file was re-read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Logic changes.&lt;/strong&gt; A pipeline change is reconciled the same way: CocoIndex compares the new output against what is already in Postgres and applies only the difference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change the file patterns&lt;/strong&gt; (&lt;code&gt;included_patterns&lt;/code&gt; / &lt;code&gt;excluded_patterns&lt;/code&gt;): files that now match are added automatically; files that no longer match have their rows deleted automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune the chunking&lt;/strong&gt; (chunk size, overlap): files are re-chunked, producing the same partial-overlap diff shown above: unchanged chunks are no-ops, new chunks are embedded and inserted, dropped chunks are deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the embedding model&lt;/strong&gt;: the vectors genuinely change, so all embeddings are recomputed, but row identity is stable: it is an in-place update of the &lt;code&gt;embedding&lt;/code&gt; column, with no rows added or removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A catch-up run (&lt;code&gt;cocoindex update main&lt;/code&gt;) does this once and exits; live mode (&lt;code&gt;cocoindex update -L main&lt;/code&gt;) keeps watching and applies each change as you code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;The full, runnable example is in the CocoIndex repo: &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/code_embedding" rel="noopener noreferrer"&gt;examples/code_embedding&lt;/a&gt;, with a walkthrough in the &lt;a href="https://cocoindex.io/docs/examples/index-codebase/" rel="noopener noreferrer"&gt;index-codebase example&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Point &lt;code&gt;sourcedir&lt;/code&gt; at your own repository.&lt;/li&gt;
&lt;li&gt;Swap the embedding model or the target store; the pipeline is plain Python.&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://cocoindex.io/docs/" rel="noopener noreferrer"&gt;CocoIndex V1 docs&lt;/a&gt; for more on incremental processing, components, and targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues and contributions are welcome on &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Beyond Embeddings: Building Metadata-Rich Indexes from Academic PDFs</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Fri, 11 Jul 2025 06:20:41 +0000</pubDate>
      <link>https://dev.to/cocoindex/beyond-embeddings-building-metadata-rich-indexes-from-academic-pdfs-2cde</link>
      <guid>https://dev.to/cocoindex/beyond-embeddings-building-metadata-rich-indexes-from-academic-pdfs-2cde</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/academic-papers-indexing/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first page of a paper holds almost everything you'd want to query: title, authors, abstract. But it's locked in PDF prose. This walkthrough reads just that page, hands the text to an LLM with a strict schema, gets back clean typed JSON, and embeds the metadata so you can search papers by meaning. One PDF fans out into three Postgres tables, and CocoIndex keeps all three in sync as the folder changes.&lt;/p&gt;

&lt;p&gt;The full code is in the &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/paper_metadata" rel="noopener noreferrer"&gt;CocoIndex v1 repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'll build
&lt;/h2&gt;

&lt;p&gt;Each PDF flows through one processing component: slice out the first page, pull its text, extract metadata with the LLM, and embed the title and abstract. The component declares rows into three tables.&lt;/p&gt;

&lt;p&gt;The three tables serve three kinds of queries. &lt;code&gt;paper_metadata&lt;/code&gt; holds one row per paper for lookups and joins. &lt;code&gt;author_papers&lt;/code&gt; maps each author name to a filename, so "all the papers by Jeff Dean" is a plain SQL &lt;code&gt;WHERE&lt;/code&gt;. &lt;code&gt;metadata_embeddings&lt;/code&gt; holds one vector per title and per abstract chunk, for semantic search with pgvector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the models and rows
&lt;/h2&gt;

&lt;p&gt;Two pydantic models describe what the LLM must return. Validation is the contract: a malformed response fails loudly instead of writing junk.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=models.py&lt;br&gt;
from pydantic import BaseModel, Field&lt;/p&gt;

&lt;p&gt;class AuthorModel(BaseModel):&lt;br&gt;
    name: str&lt;br&gt;
    email: str | None = None&lt;br&gt;
    affiliation: str | None = None&lt;/p&gt;

&lt;p&gt;class PaperMetadataModel(BaseModel):&lt;br&gt;
    title: str&lt;br&gt;
    authors: list[AuthorModel] = Field(default_factory=list)&lt;br&gt;
    abstract: str&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


The pydantic models are the LLM contract only. What lands in Postgres is described separately, by three dataclasses, one per table. Keeping the two layers apart lets the table shape differ from the LLM's: rows add `filename` and `num_pages`, and authors are stored as plain dicts. The `embedding` field is annotated with the embedder so CocoIndex infers the vector size:



```python title=main.py
import uuid
from dataclasses import dataclass
from typing import Annotated
from numpy.typing import NDArray

import cocoindex as coco
from cocoindex.ops.sentence_transformers import SentenceTransformerEmbedder

EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
EMBEDDER = coco.ContextKey[SentenceTransformerEmbedder]("embedder", detect_change=True)

@dataclass
class PaperMetadataRow:
    filename: str
    title: str
    authors: list[dict[str, str | None]]
    abstract: str
    num_pages: int

@dataclass
class AuthorPaperRow:
    author_name: str
    filename: str

@dataclass
class MetadataEmbeddingRow:
    id: uuid.UUID
    filename: str
    location: str
    text: str
    embedding: Annotated[NDArray, EMBEDDER]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The database pool and the embedder are created once, in the app lifespan:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
import asyncpg&lt;br&gt;
from cocoindex.connectors import postgres&lt;/p&gt;

&lt;p&gt;PG_DB = coco.ContextKey&lt;a href="https://dev.to%22paper_metadata_db%22"&gt;asyncpg.Pool&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.lifespan&lt;br&gt;
async def coco_lifespan(builder: coco.EnvironmentBuilder) -&amp;gt; AsyncIterator[None]:&lt;br&gt;
    async with asyncpg.create_pool(os.getenv("POSTGRES_URL")) as pool:&lt;br&gt;
        builder.provide(PG_DB, pool)&lt;br&gt;
        builder.provide(EMBEDDER, SentenceTransformerEmbedder(EMBED_MODEL))&lt;br&gt;
        yield&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## Read the first page

A paper can be large. The Attention Is All You Need PDF in the sample folder is 2,215,244 bytes, and other arXiv PDFs run to dozens of pages. The metadata we want lives on page one, so we slice it out with pypdf and never send the rest anywhere:



```python title=main.py
import io
from pypdf import PdfReader, PdfWriter

@dataclass
class PaperBasicInfo:
    num_pages: int
    first_page: bytes

@coco.fn
def extract_basic_info(content: bytes) -&amp;gt; PaperBasicInfo:
    """Extract first page bytes and page count from a PDF."""
    reader = PdfReader(io.BytesIO(content))

    output = io.BytesIO()
    writer = PdfWriter()
    writer.add_page(reader.pages[0])
    writer.write(output)

    return PaperBasicInfo(num_pages=len(reader.pages), first_page=output.getvalue())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A second small function pulls the text off that single page:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
def pdf_to_markdown(content: bytes) -&amp;gt; str:&lt;br&gt;
    """Convert PDF bytes to text using pypdf."""&lt;br&gt;
    reader = PdfReader(io.BytesIO(content))&lt;br&gt;
    page_text = reader.pages[0].extract_text() if reader.pages else ""&lt;br&gt;
    return page_text or ""&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


pypdf keeps the example dependency-light. If your PDFs are heavier on layout, swap this function's body for [Marker](https://github.com/datalab-to/marker) or [Docling](https://github.com/docling-project/docling); the rest of the pipeline does not change.

## Extract metadata with an LLM

One call turns the first-page prose into typed fields. The system prompt pins the schema, `response_format` forces JSON, `temperature=0` keeps it deterministic, and `model_validate_json` parses the response into the pydantic model:



```python title=main.py
import functools
from openai import OpenAI

LLM_MODEL = "gpt-4o"

@functools.cache
def openai_client() -&amp;gt; OpenAI:
    return OpenAI()

@coco.fn
def extract_metadata(markdown: str) -&amp;gt; PaperMetadataModel:
    """Extract paper metadata from first-page text using an LLM."""
    client = openai_client()
    response = client.chat.completions.create(
        model=LLM_MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "You extract metadata from academic paper first pages. "
                    "Return only JSON with keys: title, authors, abstract. "
                    "authors is a list of {name, email, affiliation}. "
                    "Use null for missing fields."
                ),
            },
            {
                "role": "user",
                "content": markdown[:4000],
            },
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )

    content = response.choices[0].message.content
    if not content:
        raise RuntimeError("LLM returned empty content.")
    return PaperMetadataModel.model_validate_json(content)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Process one paper into three tables
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;process_file&lt;/code&gt; is the whole per-paper pipeline: read, slice, extract, embed, declare. &lt;code&gt;memo=True&lt;/code&gt; caches it, so an unchanged PDF is skipped on the next run:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
from cocoindex.ops.text import CustomLanguageConfig, RecursiveSplitter&lt;br&gt;
from cocoindex.resources.file import FileLike&lt;/p&gt;

&lt;p&gt;_abstract_splitter = RecursiveSplitter(&lt;br&gt;
    custom_languages=[&lt;br&gt;
        CustomLanguageConfig(&lt;br&gt;
            language_name="abstract",&lt;br&gt;
            separators_regex=[r"[.?!]+\s+", r"[:;]\s+", r",\s+", r"\s+"],&lt;br&gt;
        )&lt;br&gt;
    ]&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn(memo=True)&lt;br&gt;
async def process_file(&lt;br&gt;
    file: FileLike,&lt;br&gt;
    metadata_table: postgres.TableTarget[PaperMetadataRow],&lt;br&gt;
    author_table: postgres.TableTarget[AuthorPaperRow],&lt;br&gt;
    embedding_table: postgres.TableTarget[MetadataEmbeddingRow],&lt;br&gt;
) -&amp;gt; None:&lt;br&gt;
    content = await file.read()&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;basic_info = extract_basic_info(content)
first_page_md = pdf_to_markdown(basic_info.first_page)
metadata = extract_metadata(first_page_md)

metadata_table.declare_row(
    row=PaperMetadataRow(
        filename=str(file.file_path.path),
        title=metadata.title,
        authors=[a.model_dump() for a in metadata.authors],
        abstract=metadata.abstract,
        num_pages=basic_info.num_pages,
    ),
)

for author in metadata.authors:
    if author.name:
        author_table.declare_row(
            row=AuthorPaperRow(
                author_name=author.name,
                filename=str(file.file_path.path),
            ),
        )

title_embedding = await coco.use_context(EMBEDDER).embed(metadata.title)
embedding_table.declare_row(
    row=MetadataEmbeddingRow(
        id=uuid.uuid4(),
        filename=str(file.file_path.path),
        location="title",
        text=metadata.title,
        embedding=title_embedding,
    ),
)

abstract_chunks = _abstract_splitter.split(
    metadata.abstract,
    chunk_size=500,
    min_chunk_size=200,
    chunk_overlap=150,
    language="abstract",
)
for chunk in abstract_chunks:
    embedding_table.declare_row(
        row=MetadataEmbeddingRow(
            id=uuid.uuid4(),
            filename=str(file.file_path.path),
            location="abstract",
            text=chunk.text,
            embedding=await coco.use_context(EMBEDDER).embed(chunk.text),
        ),
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Run the numbers for one concrete paper. Attention Is All You Need has 8 authors, and its 1,139-character abstract splits into 3 chunks with these settings (sentence-aware separators, chunk size 500, overlap 150). So the function declares 13 rows: 1 metadata row, 8 author rows, and 4 embedding rows (the title plus the 3 chunks):

The abstract is chunked because a long abstract blurs into a single vector; per-chunk embeddings keep search sharp. And everything is declarative: remove the PDF from the folder and all 13 rows across the three tables are cleaned up on the next run.

## Wire up the app

`app_main` mounts the three target tables, walks the folder of PDFs, and runs one `process_file` per file:



```python title=main.py
import pathlib
from cocoindex.connectors import localfs
from cocoindex.resources.file import PatternFilePathMatcher

PG_SCHEMA_NAME = "coco_examples_v1"

@coco.fn
async def app_main(sourcedir: pathlib.Path) -&amp;gt; None:
    metadata_table = await postgres.mount_table_target(
        PG_DB,
        table_name="paper_metadata",
        table_schema=await postgres.TableSchema.from_class(
            PaperMetadataRow,
            primary_key=["filename"],
        ),
        pg_schema_name=PG_SCHEMA_NAME,
    )
    author_table = await postgres.mount_table_target(
        PG_DB,
        table_name="author_papers",
        table_schema=await postgres.TableSchema.from_class(
            AuthorPaperRow,
            primary_key=["author_name", "filename"],
        ),
        pg_schema_name=PG_SCHEMA_NAME,
    )
    embedding_table = await postgres.mount_table_target(
        PG_DB,
        table_name="metadata_embeddings",
        table_schema=await postgres.TableSchema.from_class(
            MetadataEmbeddingRow,
            primary_key=["id"],
        ),
        pg_schema_name=PG_SCHEMA_NAME,
    )

    files = localfs.walk_dir(
        sourcedir,
        recursive=True,
        path_matcher=PatternFilePathMatcher(included_patterns=["**/*.pdf"]),
        live=True,
    )
    await coco.mount_each(
        process_file, files.items(), metadata_table, author_table, embedding_table
    )

app = coco.App(
    coco.AppConfig(name="PaperMetadataV1"),
    app_main,
    sourcedir=pathlib.Path("./papers"),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;mount_table_target&lt;/code&gt; creates and manages each table: schema, idempotent upserts, and removal of rows whose source vanished. &lt;code&gt;walk_dir(..., live=True)&lt;/code&gt; means the source can watch the folder; drop a new PDF in and it gets indexed without re-running anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Start Postgres with pgvector (the repo ships a compose file), set &lt;code&gt;POSTGRES_URL&lt;/code&gt; and &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, and build the index. The example folder ships sample papers, including Attention Is All You Need, so it works out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; ../../dev/postgres.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
cocoindex update main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run extracts and embeds everything; re-runs only process changed files. Add &lt;code&gt;-L&lt;/code&gt; (&lt;code&gt;cocoindex update -L main&lt;/code&gt;) to watch the folder and update the index live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query the index
&lt;/h2&gt;

&lt;p&gt;Semantic search embeds the query with the same model and asks Postgres for the nearest vectors; &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; is pgvector's cosine distance:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
async def query_once(pool, embedder, query: str, *, top_k: int = 5) -&amp;gt; None:&lt;br&gt;
    query_vec = await embedder.embed(query)&lt;br&gt;
    async with pool.acquire() as conn:&lt;br&gt;
        rows = await conn.fetch(&lt;br&gt;
            """&lt;br&gt;
            SELECT filename, location, text, embedding &amp;lt;=&amp;gt; $1 AS distance&lt;br&gt;
            FROM "coco_examples_v1"."metadata_embeddings"&lt;br&gt;
            ORDER BY distance ASC&lt;br&gt;
            LIMIT $2&lt;br&gt;
            """,&lt;br&gt;
            query_vec,&lt;br&gt;
            top_k,&lt;br&gt;
        )&lt;br&gt;
    for r in rows:&lt;br&gt;
        score = 1.0 - float(r["distance"])&lt;br&gt;
        print(f"[{score:.3f}] {r['filename']} ({r['location']})")&lt;br&gt;
        print(f"    {r['text']}")&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;




```sh
python main.py "sequence models without recurrence"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Matches come back from both titles and abstract chunks, with the &lt;code&gt;location&lt;/code&gt; column telling you which. The author index needs no embeddings at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"coco_examples_v1"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"author_papers"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Ashish Vaswani'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The shape here generalizes past papers: any document family with a metadata-rich region (contracts, invoices, intake forms) can use the same slice-extract-validate-embed pipeline. Swap the source for &lt;a href="https://cocoindex.io/blogs/s3-incremental-etl" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; or &lt;a href="https://cocoindex.io/docs/connectors/google_drive/" rel="noopener noreferrer"&gt;Google Drive&lt;/a&gt;, swap the target for &lt;a href="https://cocoindex.io/docs/connectors/qdrant/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;, or feed the metadata tables to an agent. For full-text embedding of the whole PDF body, see &lt;a href="https://cocoindex.io/blogs/text-embeddings-101" rel="noopener noreferrer"&gt;text embeddings 101&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>showdev</category>
      <category>python</category>
    </item>
    <item>
      <title>Build Real-Time Knowledge Graphs from Documents Using CocoIndex + Kuzu (with LLMs &amp; Live Updates)</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Wed, 04 Jun 2025 05:01:25 +0000</pubDate>
      <link>https://dev.to/cocoindex/build-real-time-knowledge-graphs-from-documents-using-cocoindex-kuzu-with-llms-live-updates-n1b</link>
      <guid>https://dev.to/cocoindex/build-real-time-knowledge-graphs-from-documents-using-cocoindex-kuzu-with-llms-live-updates-n1b</guid>
      <description>&lt;p&gt;A blazing-fast, end-to-end open source pipeline for turning documents into queryable knowledge graphs using LLMs, &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex&lt;/a&gt;, and the &lt;a href="https://github.com/kuzudb/kuzu" rel="noopener noreferrer"&gt;Kuzu&lt;/a&gt; graph database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F591z8fysofqen9ef14hz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F591z8fysofqen9ef14hz.png" alt="Kuzu + CocoIndex" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔍 Why Real-Time Knowledge Graphs?&lt;br&gt;
If you're working with unstructured documents and want to make the data inside searchable, relational, and up-to-date — this guide is for you. With just ~200 lines of Python, we’ll show you how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract semantic relationships using an LLM&lt;/li&gt;
&lt;li&gt;Stream structured graph data into Kuzu&lt;/li&gt;
&lt;li&gt;Build a real-time, self-updating knowledge base&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What You’ll Use
&lt;/h2&gt;

&lt;p&gt;✅ CocoIndex&lt;br&gt;
&lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;https://github.com/cocoindex-io/cocoindex&lt;/a&gt;&lt;br&gt;
A high-performance data transformation engine using a dataflow programming model. It enables incremental processing, modular plugins, and live updates for knowledge graphs.&lt;/p&gt;

&lt;p&gt;✅ Kuzu&lt;br&gt;
&lt;a href="https://github.com/kuzudb/kuzu" rel="noopener noreferrer"&gt;https://github.com/kuzudb/kuzu&lt;/a&gt;&lt;br&gt;
An open-source, fast, and lightweight graph database designed for high performance. It’s easy to run locally or in production, and now natively supported in CocoIndex.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2310yix59gqq4m3elqih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2310yix59gqq4m3elqih.png" alt="kuzu" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ Large Language Models (LLMs)&lt;br&gt;
Used for extracting subject-object-predicate relationships from document content.&lt;/p&gt;

&lt;p&gt;We understand preparing data is highly use-case based and there is no one-size-fits-all solution. &lt;/p&gt;

&lt;p&gt;We take the composition approach, and instead of building everything, we provide native plugins to embrace the ecosystem and make it easier to plug in and swap any module by standardizing the interface - exactly like LEGO.&lt;/p&gt;

&lt;p&gt;If you are using CocoIndex to build your knowledge graph, you can use Kuzu as a target graph data store.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to map to Kuzu in CocoIndex
&lt;/h2&gt;

&lt;p&gt;The GraphDB interface in CocoIndex is standardized, if you are already using Neo4j, you just need to switch the configuration to export to Kuzu as below.&lt;br&gt;
CocoIndex supports exporting to Kuzu through its API server. You can bring up a Kuzu API server locally by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KUZU_DB_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kuzudb
&lt;span class="nv"&gt;KUZU_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8123
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; kuzu &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUZU_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:8000 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUZU_DB_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/database kuzudb/api-server:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your CocoIndex flow, you need to add the Kuzu connection spec to your flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;kuzu_conn_spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_auth_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KuzuConnection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;KuzuConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;api_server_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What does it look like to build an indexing flow with CocoIndex + Kuzu
&lt;/h2&gt;

&lt;p&gt;A CocoIndex knowledge graph example that got the most love is to build knowledge graph with LLM, here is a detailed &lt;a href="https://cocoindex.io/blogs/knowledge-graph-for-docs" rel="noopener noreferrer"&gt;step-by-step blog&lt;/a&gt;. &lt;br&gt;
In the project, we process a list of documents,and use LLM to extract relationships between the concepts in each document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fth3bcgdsayfemtin5gdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fth3bcgdsayfemtin5gdt.png" alt="Knowledge Graph Relationship" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will generate two kinds of relationships from the documents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Relationships between subjects and objects. E.g., "CocoIndex supports Incremental Processing"&lt;/li&gt;
&lt;li&gt;Mentions of entities in a document. E.g., "core/basics.mdx" mentions CocoIndex and Incremental Processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The indexing flow looks like this for Kuzu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3keh1smrkeckt3uy0d3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3keh1smrkeckt3uy0d3.png" alt="Indexing flow" width="800" height="531"&gt;&lt;/a&gt;&lt;br&gt;
The code is available &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/docs_to_knowledge_graph" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ingest the documents into CocoIndex&lt;/li&gt;
&lt;li&gt;Process the documents, for each document:

&lt;ol&gt;
&lt;li&gt; Map document nodes: Use LLM to generate summary, and map the documents to Graph nodes in Kuzu.&lt;/li&gt;
&lt;li&gt; Map relationship nodes: Use LLM to extract relationships, and export the relationships to Kuzu. &lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notably, it only takes ~200 lines of python to have a production ready knowledge graph; including class definitions, prompts, and configs.&lt;/p&gt;

&lt;p&gt;To highlight how the relationship extraction works, you will define a python class for structured extraction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclasses.dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Relationship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Describe a relationship between two entities.
    Subject and object should be Core CocoIndex concepts only, should be nouns. For example, `CocoIndex`, `Incremental Processing`, `ETL`,  `Data` etc.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have a predefined set of ontology, you can skip the entity extraction use existing entities.&lt;/p&gt;

&lt;p&gt;Call a transformation in the flow to extract the relationships from the document.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;data_scope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# extract relationships from document
&lt;/span&gt;    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relationships&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExtractByLlm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;llm_spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LlmSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="c1"&gt;# Supported LLM: https://cocoindex.io/docs/ai/llm
&lt;/span&gt;                &lt;span class="n"&gt;api_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LlmApiType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OPENAI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Relationship&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please extract relationships from CocoIndex documents. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Focus on concepts and ignore examples and code. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could use CocoInsight to verify each pair of the relationships.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03kx68yfwsmyvc1b683t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03kx68yfwsmyvc1b683t.png" alt="CocoInsight" width="799" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and then collect the relationship use &lt;code&gt;entity_relationship&lt;/code&gt; collector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relationships&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# relationship between two entities
&lt;/span&gt;    &lt;span class="n"&gt;entity_relationship&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GeneratedField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;predicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CocoIndex follows a dataflow programming model. &lt;br&gt;
Rather than defining data operations like creations, updates or deletions, developers only need to focus on transformations or formulas based on source data.&lt;br&gt;
The framework takes care of the data operations such as when to create, update, or delete.&lt;/p&gt;

&lt;p&gt;Once you have collected the relationships, you can directly map it to Kuzu as below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;entity_relationship&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_relationship&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Kuzu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;conn_spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Relationships&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RELATIONSHIP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NodeFromFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TargetFieldMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NodeFromFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Entity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="n"&gt;cocoindex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TargetFieldMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;primary_key_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazingly, while working on this Kuzu example, I had a previous &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/docs_to_knowledge_graph" rel="noopener noreferrer"&gt;flow&lt;/a&gt; that I ran locally with Neo4j. &lt;br&gt;
It was instant to export to Kuzu. CocoIndex is based on incremental processing, and if you have already run this flow before and just switched targets, the intermediate transformation results can be reused.&lt;/p&gt;

&lt;p&gt;To run the &lt;a href="https://github.com/kuzudb/explorer" rel="noopener noreferrer"&gt;Kuzu Explorer&lt;/a&gt; - an open source UI for Kuzu, you need to first bring down the Kuzu API server.&lt;/p&gt;

&lt;p&gt;And then you can run the following command to start the Kuzu Explorer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KUZU_EXPLORER_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8124
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; kuzu-explorer &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUZU_EXPLORER_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:8000  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KUZU_DB_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;:/database &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;READ_ONLY  kuzudb/explorer:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could then access the explorer at &lt;a href="http://localhost:8124" rel="noopener noreferrer"&gt;http://localhost:8124&lt;/a&gt;. We could run a Cypher query to explore the graph.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ss"&gt;()&lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61923z7rjggulwdzv3gi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61923z7rjggulwdzv3gi.png" alt="Knowledge Graph Explorer" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Support us
&lt;/h2&gt;

&lt;p&gt;We are constantly improving, and more features and examples are coming soon. If this article is helpful, please drop us a star ⭐ at &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to help us grow.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stream from S3 in Real-Time: CocoIndex Brings True Incremental Processing to the Cloud</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Fri, 30 May 2025 17:46:31 +0000</pubDate>
      <link>https://dev.to/cocoindex/stream-from-s3-in-real-time-cocoindex-brings-true-incremental-processing-to-the-cloud-1d1l</link>
      <guid>https://dev.to/cocoindex/stream-from-s3-in-real-time-cocoindex-brings-true-incremental-processing-to-the-cloud-1d1l</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/s3-incremental-etl/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex&lt;/a&gt; has native support for &lt;a href="https://cocoindex.io/docs/connectors/amazon_s3/" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; as a data source. The pipeline reads Markdown files from an S3 bucket, chunks and embeds them, and writes the vectors to a Postgres &lt;a href="https://cocoindex.io/docs/connectors/postgres/" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; table for semantic search.&lt;/p&gt;

&lt;p&gt;The point of the example is incremental processing: adding one object to the bucket embeds one object, not the whole bucket. It is &lt;a href="https://cocoindex.io/blogs/text-embeddings-101" rel="noopener noreferrer"&gt;Semantic Search 101&lt;/a&gt; with the source swapped from a local folder to S3; everything downstream is the same.&lt;/p&gt;

&lt;p&gt;The full project is in the &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/amazon_s3_embedding" rel="noopener noreferrer"&gt;CocoIndex v1 repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why incremental processing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cocoindex.io/docs/programming_guide/core_concepts/" rel="noopener noreferrer"&gt;Incremental processing&lt;/a&gt; processes only the data that changed since the last run, instead of reprocessing everything. It matters most in three situations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High freshness requirements.&lt;/strong&gt; For user-facing search, stale results are a real problem: when someone updates a document, they expect it reflected in search right away. Feed a stale result into an AI agent and the model answers from out-of-date information, often without anyone noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expensive transforms.&lt;/strong&gt; When each item goes through an embedding model or an LLM, the transform costs far more than the retrieval. Recomputing unchanged items burns money for no benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large datasets.&lt;/strong&gt; With terabytes in S3, reprocessing everything on each new file is impractical on time, cost, and compute. Incremental processing keeps the work proportional to the change, not the corpus.&lt;/p&gt;

&lt;p&gt;A useful way to think about it: if &lt;code&gt;T&lt;/code&gt; is the staleness you can tolerate, and you do not want to recompute the whole corpus every &lt;code&gt;T&lt;/code&gt;, you need incremental processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How CocoIndex processes only what changed
&lt;/h2&gt;

&lt;p&gt;CocoIndex tracks each unit of work by a fingerprint of its input and the transform code. On the next run, anything whose fingerprint is unchanged is skipped, and its downstream rows are left untouched.&lt;/p&gt;

&lt;p&gt;The granularity is finer than the file. If a Markdown file has many chunks and you edit one paragraph, only the chunks that actually changed are re-embedded; the rest reuse their cached vectors:&lt;/p&gt;

&lt;p&gt;That is where the savings come from. The expensive embedding calls run only for content CocoIndex has not seen, and the vector store ends up consistent either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The indexing flow
&lt;/h2&gt;

&lt;p&gt;The flow reads Markdown files from S3, and for each file splits it into chunks, embeds each chunk, and declares a row in the vector table:&lt;/p&gt;

&lt;p&gt;Each file is handled by its own &lt;a href="https://cocoindex.io/docs/programming_guide/processing_component/" rel="noopener noreferrer"&gt;processing component&lt;/a&gt;, so change detection and parallelism are per file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the pipeline
&lt;/h2&gt;

&lt;p&gt;Shared resources (the Postgres pool, the S3 client, and the embedder) are provided once in the app lifespan and read anywhere with &lt;code&gt;coco.use_context&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
import cocoindex as coco&lt;br&gt;
from cocoindex.connectors import amazon_s3, postgres&lt;br&gt;
from cocoindex.ops.sentence_transformers import SentenceTransformerEmbedder&lt;/p&gt;

&lt;p&gt;EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"&lt;/p&gt;

&lt;p&gt;PG_DB = coco.ContextKey&lt;a href="https://dev.to%22s3_embedding_db%22"&gt;asyncpg.Pool&lt;/a&gt;&lt;br&gt;
S3_CLIENT = coco.ContextKey&lt;a href="https://dev.to%22s3_client%22"&gt;AioBaseClient&lt;/a&gt;&lt;br&gt;
EMBEDDER = coco.ContextKey&lt;a href="https://dev.to%22embedder%22,%20detect_change=True"&gt;SentenceTransformerEmbedder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.lifespan&lt;br&gt;
async def coco_lifespan(builder: coco.EnvironmentBuilder) -&amp;gt; AsyncIterator[None]:&lt;br&gt;
    async with asyncpg.create_pool(DATABASE_URL) as pool:&lt;br&gt;
        builder.provide(PG_DB, pool)&lt;br&gt;
        builder.provide(EMBEDDER, SentenceTransformerEmbedder(EMBED_MODEL))&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    session = aiobotocore.session.get_session()
    async with session.create_client("s3") as s3_client:
        builder.provide(S3_CLIENT, s3_client)
        yield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`detect_change=True` on the embedder means swapping the model re-runs only the embedding step, not the whole pipeline.

The target row is a plain dataclass. The `embedding` column is annotated with the embedder so CocoIndex knows the vector dimension:



```python title=main.py
@dataclass
class DocEmbedding:
    id: int
    filename: str
    chunk_start: int
    chunk_end: int
    text: str
    embedding: Annotated[NDArray, EMBEDDER]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Chunk, embed, declare
&lt;/h2&gt;

&lt;p&gt;Each chunk becomes one row. &lt;code&gt;process_chunk&lt;/code&gt; embeds the chunk text and declares the row; CocoIndex works out whether that is an insert or an update:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def process_chunk(&lt;br&gt;
    chunk: Chunk,&lt;br&gt;
    filename: str,&lt;br&gt;
    id_gen: IdGenerator,&lt;br&gt;
    table: postgres.TableTarget[DocEmbedding],&lt;br&gt;
) -&amp;gt; None:&lt;br&gt;
    table.declare_row(&lt;br&gt;
        row=DocEmbedding(&lt;br&gt;
            id=await id_gen.next_id(chunk.text),&lt;br&gt;
            filename=filename,&lt;br&gt;
            chunk_start=chunk.start.char_offset,&lt;br&gt;
            chunk_end=chunk.end.char_offset,&lt;br&gt;
            text=chunk.text,&lt;br&gt;
            embedding=await coco.use_context(EMBEDDER).embed(chunk.text),&lt;br&gt;
        ),&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`process_file` reads one S3 file, splits it into chunks, and maps `process_chunk` over them. `memo=True` caches per-file work, so an unchanged file is skipped entirely:



```python title=main.py
_splitter = RecursiveSplitter()

@coco.fn(memo=True)
async def process_file(
    file: amazon_s3.S3File,
    table: postgres.TableTarget[DocEmbedding],
) -&amp;gt; None:
    text = await file.read_text()
    chunks = _splitter.split(
        text, chunk_size=2000, chunk_overlap=500, language="markdown"
    )
    id_gen = IdGenerator()
    await coco.map(process_chunk, chunks, file.file_path.path.as_posix(), id_gen, table)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Wire up the app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app_main&lt;/code&gt; mounts the pgvector table target, lists the Markdown objects in the bucket, and fans out one &lt;code&gt;process_file&lt;/code&gt; component per file with &lt;code&gt;mount_each&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def app_main() -&amp;gt; None:&lt;br&gt;
    target_table = await postgres.mount_table_target(&lt;br&gt;
        PG_DB,&lt;br&gt;
        table_name=TABLE_NAME,&lt;br&gt;
        table_schema=await postgres.TableSchema.from_class(&lt;br&gt;
            DocEmbedding, primary_key=["id"]&lt;br&gt;
        ),&lt;br&gt;
        pg_schema_name=PG_SCHEMA_NAME,&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client = coco.use_context(S3_CLIENT)
files = amazon_s3.list_objects(
    client,
    S3_BUCKET,
    prefix=S3_PREFIX,
    path_matcher=PatternFilePathMatcher(included_patterns=["**/*.md"]),
)
await coco.mount_each(process_file, files.items(), target_table)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;app = coco.App(&lt;br&gt;
    coco.AppConfig(name="AmazonS3Embedding"),&lt;br&gt;
    app_main,&lt;br&gt;
)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`amazon_s3.list_objects` returns the objects under the prefix; `files.items()` yields `(key, file)` pairs, and `mount_each` gives each file a stable component path keyed on its S3 path.

## Set up the S3 bucket

Follow the [Amazon S3 connector docs](https://cocoindex.io/docs/connectors/amazon_s3/) to create a bucket and grant read access. The pipeline needs read-only S3 permissions (`AmazonS3ReadOnlyAccess` is enough), and standard AWS credentials in the environment. Point the pipeline at the bucket with a `.env` file:



```sh
POSTGRES_URL=postgresql://cocoindex:cocoindex@localhost/cocoindex
S3_BUCKET=your-bucket-name
S3_PREFIX=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then upload your Markdown files to the bucket.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run the pipeline
&lt;/h2&gt;

&lt;p&gt;Build the index. This is a one-shot catch-up over the objects in the bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cocoindex update main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run embeds everything. Every run after that does only the new work: new and changed files are processed, unchanged files are skipped, and within a changed file only the changed chunks are re-embedded.&lt;/p&gt;

&lt;p&gt;For fresh data, schedule the update on whatever cadence you need (cron, Airflow, GitHub Actions, an Azure or Lambda function). Because re-runs only touch what changed, running the pipeline often is cheap. If you need event-driven, second-level freshness instead of scheduled runs, use one of CocoIndex's &lt;a href="https://cocoindex.io/docs/connectors/" rel="noopener noreferrer"&gt;streaming sources&lt;/a&gt; (Kafka, Iggy, or Valkey) as the source of your pipeline.&lt;/p&gt;

&lt;p&gt;Once the index is built, query it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="s2"&gt;"your query"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>programming</category>
      <category>showdev</category>
      <category>python</category>
    </item>
    <item>
      <title>How to build image search with semantic understanding</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Sun, 25 May 2025 23:27:45 +0000</pubDate>
      <link>https://dev.to/cocoindex/how-to-build-image-search-with-semantic-understanding-1la2</link>
      <guid>https://dev.to/cocoindex/how-to-build-image-search-with-semantic-understanding-1la2</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/live-image-search/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Search a folder of photos for "long neck" and a filename search returns nothing, unless someone named the giraffe photo that way.&lt;/p&gt;

&lt;p&gt;This post builds image search that matches by meaning instead: you type "long neck" and the giraffe comes back, without captions or manual labels. Try it on the 4 photos that ship with the example. The scores are real CLIP scores.&lt;/p&gt;

&lt;p&gt;The model that makes this work is &lt;a href="https://huggingface.co/openai/clip-vit-large-patch14" rel="noopener noreferrer"&gt;CLIP&lt;/a&gt;. It embeds an image and a piece of text into the &lt;em&gt;same&lt;/em&gt; vector space, so a text query and a matching picture land near each other. CocoIndex v1 walks a local image folder, embeds each picture with CLIP, and stores the vectors in &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;. The index runs in &lt;a href="https://cocoindex.io/docs/programming_guide/live_mode/" rel="noopener noreferrer"&gt;live mode&lt;/a&gt; inside a FastAPI server, so a photo you drop into the folder becomes searchable as soon as it is embedded, with no rebuild step.&lt;/p&gt;

&lt;p&gt;The full code is in the &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/image_search" rel="noopener noreferrer"&gt;CocoIndex repo&lt;/a&gt; and runs on CocoIndex v1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why search photos by meaning
&lt;/h2&gt;

&lt;p&gt;You write the transformation in plain Python, &lt;code&gt;target_state = transformation(source_state)&lt;/code&gt;. CocoIndex handles the rest: the file watcher, change tracking, &lt;a href="https://cocoindex.io/docs/programming_guide/function/#memoization" rel="noopener noreferrer"&gt;memoization&lt;/a&gt;, and a managed Qdrant collection, with change tracking and memoization running in its Rust engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;p&gt;Each image is its own &lt;a href="https://cocoindex.io/docs/programming_guide/processing_component/" rel="noopener noreferrer"&gt;processing component&lt;/a&gt;: read the bytes, embed them with the CLIP image encoder, and declare a Qdrant point. There is no text to chunk, so the indexing path is one embedding per image.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared image-text space
&lt;/h2&gt;

&lt;p&gt;CLIP is trained to place a picture and its description near each other in one embedding space. At index time we only ever embed pixels (&lt;code&gt;get_image_features&lt;/code&gt;); at query time we embed text (&lt;code&gt;get_text_features&lt;/code&gt;). Both produce 768 floats in the same space, so "long neck" ends up closest to the giraffe photo even though that image was never described in words.&lt;/p&gt;

&lt;p&gt;The vector database never learns that one vector came from an image and the other from text. It returns nearest neighbors by cosine similarity. A new image therefore needs no description to become searchable; adding the file to the folder is enough.&lt;/p&gt;

&lt;p&gt;CLIP's image-to-text scores sit in a narrow band. In the post's demo, the best match for each query scores 0.21 to 0.23 and the other photos 0.11 to 0.19. Search ranks by those scores, so the order matters more than the absolute value.&lt;/p&gt;

&lt;h2&gt;
  
  
  One image, end to end
&lt;/h2&gt;

&lt;p&gt;The stepper below follows a single photo through &lt;code&gt;process_file&lt;/code&gt; before the code sections. Use the buttons, or let it play.&lt;/p&gt;

&lt;p&gt;The rest of the post builds this pipeline in 3 pieces: the shared resources, the per-image function, and the app that connects them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technologies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CLIP ViT-L/14
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/openai/clip-vit-large-patch14" rel="noopener noreferrer"&gt;CLIP ViT-L/14&lt;/a&gt; is a vision-language model that embeds images and text into a shared 768-dimensional space. We use it two ways: to embed each image at index time, and to convert a natural-language query into that same space at search time.&lt;/p&gt;

&lt;p&gt;The collection's vector size comes from &lt;code&gt;model.config.projection_dim&lt;/code&gt;, so a smaller variant like &lt;a href="https://huggingface.co/openai/clip-vit-base-patch32" rel="noopener noreferrer"&gt;CLIP ViT-B/32&lt;/a&gt; (faster, slightly lower accuracy) needs only a new &lt;code&gt;CLIP_MODEL_NAME&lt;/code&gt;, and the collection is created to match. A swap that changes the size (ViT-B/32 has 512 dimensions, against 768) makes CocoIndex drop and recreate the collection and re-embed every image on the next start. A swap that keeps the size is not noticed: the memo of &lt;code&gt;process_file&lt;/code&gt; tracks the function's code, not the &lt;code&gt;CLIP_MODEL_NAME&lt;/code&gt; constant. For that case, declare the dependency with &lt;code&gt;@coco.fn(memo=True, deps=CLIP_MODEL_NAME)&lt;/code&gt;, or stop the server and run &lt;code&gt;cocoindex update pipeline.py --full-reprocess&lt;/code&gt; once. See &lt;a href="https://cocoindex.io/docs/programming_guide/function/#memoization" rel="noopener noreferrer"&gt;memoization&lt;/a&gt; for what invalidates a memo.&lt;/p&gt;

&lt;h3&gt;
  
  
  CocoIndex
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex&lt;/a&gt; is an open-source data transformation framework for AI, with &lt;a href="https://cocoindex.io/blogs/incremental-processing" rel="noopener noreferrer"&gt;incremental processing&lt;/a&gt; built in. You declare the transformation in plain Python and your own types; change tracking and memoization run in a Rust engine underneath.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qdrant
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; is a vector database. We store the image vectors as points and query them by cosine distance through the CocoIndex &lt;a href="https://cocoindex.io/docs/connectors/qdrant/" rel="noopener noreferrer"&gt;Qdrant connector&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  FastAPI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; serves the search API and hosts the live index in its &lt;a href="https://fastapi.tiangolo.com/advanced/events/" rel="noopener noreferrer"&gt;lifespan&lt;/a&gt;, so one process both keeps the collection current and answers queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/quick_start/" rel="noopener noreferrer"&gt;Install Qdrant&lt;/a&gt;: &lt;code&gt;docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Python 3.11+ with &lt;code&gt;torch&lt;/code&gt;, &lt;code&gt;transformers&lt;/code&gt;, and &lt;code&gt;pillow&lt;/code&gt; for CLIP. &lt;code&gt;pip install -e .&lt;/code&gt; in the example folder installs them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CocoIndex v1 keeps its own processing state in a local LMDB directory, set by &lt;code&gt;COCOINDEX_DB&lt;/code&gt; (the example's &lt;code&gt;.env&lt;/code&gt; sets it to &lt;code&gt;./cocoindex.db&lt;/code&gt;), so there is no separate tracking database to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the pipeline
&lt;/h2&gt;

&lt;p&gt;The pipeline lives in &lt;a href="https://github.com/cocoindex-io/cocoindex/blob/main/examples/image_search/pipeline.py" rel="noopener noreferrer"&gt;&lt;code&gt;pipeline.py&lt;/code&gt;&lt;/a&gt;. The excerpts below leave out the standard-library, &lt;code&gt;torch&lt;/code&gt;, &lt;code&gt;transformers&lt;/code&gt;, Pillow, NumPy, and &lt;code&gt;qdrant_client&lt;/code&gt; imports; the linked file has them all. CocoIndex's Qdrant client is created in the CocoIndex lifespan and reached through the &lt;code&gt;QDRANT_DB&lt;/code&gt; context key (&lt;code&gt;api.py&lt;/code&gt; opens its own client for search):&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=pipeline.py&lt;br&gt;
import cocoindex as coco&lt;br&gt;
from cocoindex.connectors import localfs, qdrant&lt;br&gt;
from cocoindex.resources.file import FileLike, PatternFilePathMatcher&lt;br&gt;
from cocoindex.resources.schema import VectorSchema&lt;/p&gt;

&lt;p&gt;QDRANT_COLLECTION = "ImageSearch"&lt;br&gt;
CLIP_MODEL_NAME = "openai/clip-vit-large-patch14"&lt;br&gt;
TOP_K = 5&lt;/p&gt;

&lt;p&gt;def qdrant_url() -&amp;gt; str:&lt;br&gt;
    return os.getenv("QDRANT_URL", "&lt;a href="http://localhost:6334/%22" rel="noopener noreferrer"&gt;http://localhost:6334/"&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;QDRANT_DB = coco.ContextKey&lt;a href="https://dev.to%22image_search_qdrant%22"&gt;QdrantClient&lt;/a&gt;&lt;br&gt;
QDRANT_CLIENT = coco.ContextKey&lt;a href="https://dev.to%22qdrant_client%22"&gt;QdrantClient&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;




```python title=pipeline.py
@coco.lifespan
async def coco_lifespan(
    builder: coco.EnvironmentBuilder,
) -&amp;gt; AsyncIterator[None]:
    client = qdrant.create_client(qdrant_url(), prefer_grpc=True)
    builder.provide(QDRANT_DB, client)
    builder.provide(QDRANT_CLIENT, client)
    yield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;CLIP is loaded once and reused. The image encoder is an ordinary helper, so nothing about it is CocoIndex-specific. &lt;code&gt;_projected_features&lt;/code&gt; handles a return-type change between &lt;code&gt;transformers&lt;/code&gt; 4 and 5:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=pipeline.py&lt;br&gt;
@functools.cache&lt;br&gt;
def get_clip_model() -&amp;gt; tuple[CLIPModel, CLIPProcessor]:&lt;br&gt;
    model = CLIPModel.from_pretrained(CLIP_MODEL_NAME)&lt;br&gt;
    processor = CLIPProcessor.from_pretrained(CLIP_MODEL_NAME)&lt;br&gt;
    return model, processor&lt;/p&gt;

&lt;p&gt;def _projected_features(out: Any) -&amp;gt; torch.Tensor:&lt;br&gt;
    # transformers &amp;gt;=5 returns BaseModelOutputWithPooling with the projected features in pooler_output;&lt;br&gt;
    # transformers &amp;lt;5 returns the projected features tensor directly.&lt;br&gt;
    return out.pooler_output if hasattr(out, "pooler_output") else out&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;




```python title=pipeline.py
def embed_image_bytes(img_bytes: bytes) -&amp;gt; list[float]:
    model, processor = get_clip_model()
    image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
    inputs = processor(images=image, return_tensors="pt")
    with torch.no_grad():
        out = model.get_image_features(**inputs)
    return _projected_features(out)[0].tolist()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  One component per image
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;process_file&lt;/code&gt; handles a single image: read its bytes, embed them, and declare a Qdrant point keyed by a stable &lt;code&gt;uuid5&lt;/code&gt; of the path. &lt;code&gt;memo=True&lt;/code&gt; skips an image whose bytes did not change, and because each image is its own component, deleting a file removes its point:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=pipeline.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn(memo=True)&lt;br&gt;
async def process_file(&lt;br&gt;
    file: FileLike,&lt;br&gt;
    target: qdrant.CollectionTarget,&lt;br&gt;
) -&amp;gt; None:&lt;br&gt;
    content = await file.read()&lt;br&gt;
    embedding = embed_image_bytes(content)&lt;br&gt;
    row_id = _image_id(file.file_path.path)&lt;br&gt;
    point = qdrant.PointStruct(&lt;br&gt;
        id=row_id,&lt;br&gt;
        vector=embedding,&lt;br&gt;
        payload={"filename": str(file.file_path.path)},&lt;br&gt;
    )&lt;br&gt;
    target.declare_point(point)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`_image_id` is one line, `str(uuid.uuid5(uuid.NAMESPACE_URL, str(path)))`. The same path always produces the same point ID, so re-embedding an edited photo overwrites its point instead of adding a second one.

### Wire up the app

`app_main` creates the Qdrant collection (its vector size comes straight from CLIP's `projection_dim`), walks the image folder in live mode, and mounts one `process_file` per image with `mount_each`:



```python title=pipeline.py
@coco.fn
async def app_main(sourcedir: pathlib.Path) -&amp;gt; None:
    model, _ = get_clip_model()
    dim: int = model.config.projection_dim  # type: ignore[assignment]

    target_collection = await qdrant.mount_collection_target(
        QDRANT_DB,
        collection_name=QDRANT_COLLECTION,
        schema=await qdrant.CollectionSchema.create(
            vectors=qdrant.QdrantVectorDef(
                schema=VectorSchema(dtype=np.dtype(np.float32), size=dim),
                distance="cosine",
            )
        ),
    )

    files = localfs.walk_dir(
        sourcedir,
        recursive=True,
        path_matcher=PatternFilePathMatcher(
            included_patterns=["**/*.jpg", "**/*.jpeg", "**/*.png"]
        ),
        live=True,  # source supports live watch; api.py runs the app with live=True
    )
    await coco.mount_each(process_file, files.items(), target_collection)

app = coco.App(
    coco.AppConfig(name="ImageSearchQdrantV1"),
    app_main,
    sourcedir=pathlib.Path("./img"),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;walk_dir(..., live=True)&lt;/code&gt;&lt;/strong&gt; lists the matching files and, when the app runs live, keeps watching the folder. See the &lt;a href="https://cocoindex.io/docs/connectors/localfs/#live-file-watching" rel="noopener noreferrer"&gt;local file system connector&lt;/a&gt; for how live watching works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;files.items()&lt;/code&gt;&lt;/strong&gt; keys each file by its path relative to &lt;code&gt;img/&lt;/code&gt;. That key names the file's component, so CocoIndex can tell which files are new, which changed, and which disappeared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mount_collection_target&lt;/code&gt;&lt;/strong&gt; creates and manages the Qdrant collection: the schema, upserts, and removal of points whose &lt;a href="https://cocoindex.io/docs/programming_guide/target_state/" rel="noopener noreferrer"&gt;target state&lt;/a&gt; is no longer declared.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Query the index
&lt;/h2&gt;

&lt;p&gt;Search embeds the text with the same CLIP model, this time through the text encoder, and asks Qdrant for the nearest image vectors:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=pipeline.py&lt;br&gt;
def embed_query(text: str) -&amp;gt; list[float]:&lt;br&gt;
    model, processor = get_clip_model()&lt;br&gt;
    inputs = processor(text=[text], return_tensors="pt", padding=True)&lt;br&gt;
    with torch.no_grad():&lt;br&gt;
        out = model.get_text_features(**inputs)&lt;br&gt;
    return _projected_features(out)[0].tolist()&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


The FastAPI `/search` endpoint embeds the query and returns the top matches with their scores. `_qdrant_search` in `pipeline.py` calls whichever search method the installed `qdrant-client` version has:



```python title=api.py
@app.get("/search")
async def search(
    q: str = Query(..., description="Search query"),
    limit: int = Query(5, description="Number of results"),
) -&amp;gt; dict[str, Any]:
    query_embedding = pipeline.embed_query(q)

    if _client is None:
        raise RuntimeError("Qdrant client is not initialized.")

    results = pipeline._qdrant_search(
        _client,
        pipeline.QDRANT_COLLECTION,
        query_embedding,
        limit,
    )

    return {
        "results": [
            {
                "filename": (r.payload or {}).get("filename"),
                "score": r.score,
            }
            for r in results
        ]
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Run it as a live service
&lt;/h2&gt;

&lt;p&gt;There is no separate build step: &lt;code&gt;api.py&lt;/code&gt; starts the app in live mode inside the FastAPI lifespan. It blocks startup until the first sweep reports &lt;code&gt;READY&lt;/code&gt;, so the collection is queryable, then keeps watching &lt;code&gt;img/&lt;/code&gt; in the background while it serves requests:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=api.py&lt;br&gt;
@asynccontextmanager&lt;br&gt;
async def lifespan(app: FastAPI) -&amp;gt; AsyncIterator[None]:  # type: ignore[override]&lt;br&gt;
    global _client&lt;br&gt;
    async with coco.runtime():&lt;br&gt;
        _client = qdrant.create_client(pipeline.qdrant_url(), prefer_grpc=True)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # Start a live update; block startup until the initial sweep finishes so
    # the collection is queryable, then keep it running in the background.
    update_handle = pipeline.app.update(live=True)
    async for snap in update_handle.watch():
        if snap.status is coco.UpdateStatus.READY:
            break
    update_task = asyncio.create_task(update_handle.result())

    try:
        yield
    finally:
        update_task.cancel()
        with contextlib.suppress(asyncio.CancelledError):
            await update_task
        _client = None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## Changes in the folder reach the index

Once the server is up, every change to a `.jpg`, `.jpeg`, or `.png` file in `img/` reaches the collection without a command (the patterns are case-sensitive, so `IMG_001.JPG` is skipped). The interactive demo on cocoindex.io shows what each kind of change costs.

What decides each outcome:

- **New file.** The watcher reports `created`, `mount_each` mounts a component for the new path, and its memo has no entry, so CLIP runs. A file still being copied in may fire several events; each one goes through the same memo check.
- **Same bytes, new modification time.** A `touch`, or any write of identical bytes, reports a change, usually `modified`. The memo state of a local file is its modification time plus a fingerprint of its bytes. The time changed, so CocoIndex fingerprints the bytes, finds them unchanged, and skips the function. The point stays as it is.
- **New bytes.** An edited photo has a new fingerprint, so `process_file` runs again and the point is overwritten under the same ID.
- **Rename.** A move is handled as a delete of the old path and a create of the new one. The memo is keyed by the path, so the renamed photo is embedded again and gets a new point ID; the old point is deleted.
- **Delete.** The component for the path is removed, and CocoIndex deletes the point it declared.

Code changes invalidate the memo too: changing the logic of `process_file` (comments and formatting do not count) re-runs it for every image on the next start. Edits to plain helpers it calls, like `embed_image_bytes`, are not tracked. As a safety net against a watcher that silently stops (macOS FSEvents can), `walk_dir` rescans the whole folder once an hour by default; set `rescan_interval` to change that.

## Run the example

From a clone of the [CocoIndex repo](https://github.com/cocoindex-io/cocoindex), start Qdrant, install the example, and run the server. The example ships an `img/` folder with a cat, a dog, an elephant, and a giraffe:



```sh
cd cocoindex/examples/image_search
docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant
cp .env.example .env     # QDRANT_URL (defaults to the local container above)
pip install -e .
python -m uvicorn api:app --reload --host 0.0.0.0 --port 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then run the frontend and open it in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev   &lt;span class="c"&gt;# http://localhost:5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query "elephant" and the elephant ranks first; query "long neck" and the giraffe wins, then the other animals fall in line by CLIP similarity. The images have no tags or captions:&lt;/p&gt;

&lt;p&gt;Now drop another image into the &lt;code&gt;img/&lt;/code&gt; folder, for example this &lt;a href="https://www.pexels.com/photo/brown-squirrel-47547/" rel="noopener noreferrer"&gt;squirrel&lt;/a&gt;. Because the folder is watched in live mode, it becomes searchable once CLIP has embedded it, with no command to re-run. Search "a bushy tail" and it ranks first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgement
&lt;/h2&gt;

&lt;p&gt;This image search project was largely &lt;a href="https://github.com/cocoindex-io/cocoindex/pull/447" rel="noopener noreferrer"&gt;contributed by&lt;/a&gt; @par4m, &lt;a href="https://github.com/par4m" rel="noopener noreferrer"&gt;Param Arora&lt;/a&gt;, an open source developer at Google Summer of Code.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>showdev</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to build index with text embeddings</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Sat, 24 May 2025 00:23:59 +0000</pubDate>
      <link>https://dev.to/cocoindex/how-to-build-index-with-text-embeddings-j3m</link>
      <guid>https://dev.to/cocoindex/how-to-build-index-with-text-embeddings-j3m</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/text-embeddings-101/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Keyword search only finds the words you typed. Ask "how do I log back in?" and a page titled "recover your account access" never comes up, because it shares no words with your question. A page about log files does come up, because it contains "log".&lt;/p&gt;

&lt;p&gt;Semantic search matches on meaning, so a question finds its answer even when the words are different. This guide explains the 4 ideas behind it (embeddings, similarity, chunks and an index) and then builds a semantic search index over a folder of Markdown with CocoIndex v1 and Postgres. You do not need to know anything about embeddings to start.&lt;/p&gt;

&lt;p&gt;Every number in the figures is real. We computed it for this post with the model the example uses, &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;, and the 3 sample files that come with the example: the Transformer paper (&lt;code&gt;1706.03762v7.md&lt;/code&gt;), the BERT paper (&lt;code&gt;1810.04805v2.md&lt;/code&gt;) and the JSON spec, RFC 8259 (&lt;code&gt;rfc8259.md&lt;/code&gt;). The full code is in the &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/text_embedding" rel="noopener noreferrer"&gt;CocoIndex v1 repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a text embedding?
&lt;/h2&gt;

&lt;p&gt;An embedding is a list of numbers (a &lt;em&gt;vector&lt;/em&gt;) that a model gives to a piece of text. The model is trained so that texts with similar meanings get similar lists. &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; gives every text 384 numbers, whether it is 3 words or a full paragraph.&lt;/p&gt;

&lt;p&gt;No single number means "password" or "account". The meaning is in the pattern of all 384 together. You can think of the list as coordinates: it puts the text at one point in a space with 384 directions. Texts about the same thing end up near each other in that space.&lt;/p&gt;

&lt;p&gt;You cannot draw 384 directions, but you can flatten them to 2 with PCA (principal component analysis), which keeps the largest differences between the points. Here are 10 everyday phrases after that flattening:&lt;/p&gt;

&lt;p&gt;We gave the model no labels or groups. It still put "my cat sleeps all day" next to "kittens love to nap" (they share no words), and it put all the ways of saying "I can't get into my account" next to the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  How close is close?
&lt;/h2&gt;

&lt;p&gt;Search needs a number for "how close". The usual one is &lt;strong&gt;cosine similarity&lt;/strong&gt;: it compares the directions of 2 vectors. 1 means the same direction, 0 means unrelated, and a value below 0 points the other way. This model scales every vector to length 1, so cosine similarity equals the dot product: multiply the 2 lists number by number and add the results.&lt;/p&gt;

&lt;p&gt;Postgres with the &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; extension computes this for you. Its &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; operator returns the cosine &lt;strong&gt;distance&lt;/strong&gt;, which is 1 minus the similarity, so a smaller distance means a closer match. A search is then: embed the question, sort the rows by distance, keep the first few.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why split files into chunks?
&lt;/h2&gt;

&lt;p&gt;Our sample files are long. The Transformer paper in the sample folder is 46,145 characters, which is 12,746 tokens (the word pieces a model reads). &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; reads at most 256 tokens. If you embed the whole file as one text, the model uses the first 2% and drops the rest without a warning.&lt;/p&gt;

&lt;p&gt;Even a model with a longer input limit gives a file only 1 vector. A paper about many things gets 1 point somewhere between all of them, and a question about one section has to match that average. When you split the file into chunks and embed each chunk, every passage gets its own point, and the search can return the exact passage that answers the question.&lt;/p&gt;

&lt;p&gt;The example uses CocoIndex's &lt;a href="https://cocoindex.io/docs/ops/text/" rel="noopener noreferrer"&gt;&lt;code&gt;RecursiveSplitter&lt;/code&gt;&lt;/a&gt;. With &lt;code&gt;language="markdown"&lt;/code&gt; it picks split points from the Markdown structure, and it aims for chunks of about 2,000 bytes (for English text, about 2,000 characters). Neighboring chunks can overlap by up to 500 bytes, so text near a boundary often appears in both. The 3 sample files give 106 chunks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline: 3 files in, 106 rows out
&lt;/h2&gt;

&lt;p&gt;The index is a short pipeline: read the files, split each one into chunks, embed each chunk, and store one row per chunk in Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the index correct is the hard part
&lt;/h2&gt;

&lt;p&gt;You could write this pipeline as a plain loop, and it would embed every file once. Keeping the index correct as files change takes more code: on every run you must find what changed, embed only the new text, and make the table match.&lt;/p&gt;

&lt;p&gt;CocoIndex is declarative: you describe what the table should contain, not the steps to update it. You write the target as a function of the source, &lt;code&gt;target_state = transformation(source_state)&lt;/code&gt;, and its Rust engine keeps the 2 in sync. You write the transformation (chunk, embed, and declare the row each chunk should have). The engine detects changes, embeds only the text it has not seen, and applies the inserts and deletes, including cleanup when a file is removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Clone the &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex repo&lt;/a&gt; and work in &lt;code&gt;examples/text_embedding&lt;/code&gt;; the commands below expect its &lt;code&gt;main.py&lt;/code&gt; and &lt;code&gt;markdown_files/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Install &lt;a href="https://cocoindex.io/docs/connectors/postgres/" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; with the pgvector extension; that is where the embeddings live. Set the connection string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgres://cocoindex:cocoindex@localhost/cocoindex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install CocoIndex and the packages the example uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"cocoindex[postgres,sentence_transformers]"&lt;/span&gt; asyncpg pgvector numpy python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: define the row
&lt;/h3&gt;

&lt;p&gt;Each chunk becomes one row. A plain dataclass describes the columns. The &lt;code&gt;embedding&lt;/code&gt; field is annotated with the embedder, so CocoIndex gets the vector size from the model:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
import os&lt;br&gt;
import pathlib&lt;br&gt;
from dataclasses import dataclass&lt;br&gt;
from typing import Annotated, AsyncIterator&lt;br&gt;
from numpy.typing import NDArray&lt;/p&gt;

&lt;p&gt;import cocoindex as coco&lt;br&gt;
from cocoindex.ops.sentence_transformers import SentenceTransformerEmbedder&lt;/p&gt;

&lt;p&gt;DATABASE_URL = os.getenv("POSTGRES_URL", "postgres://cocoindex:cocoindex@localhost/cocoindex")&lt;br&gt;
EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"&lt;br&gt;
EMBEDDER = coco.ContextKey&lt;a href="https://dev.to%22embedder%22,%20detect_change=True"&gt;SentenceTransformerEmbedder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class DocEmbedding:&lt;br&gt;
    id: int&lt;br&gt;
    filename: str&lt;br&gt;
    chunk_start: int&lt;br&gt;
    chunk_end: int&lt;br&gt;
    text: str&lt;br&gt;
    embedding: Annotated[NDArray, EMBEDDER]&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


This is one real row from the sample files, the chunk that holds section 4 of the Transformer paper:

The database connection and the embedder are created once when the app starts (in the `coco_lifespan` function) and shared through [context](https://cocoindex.io/docs/programming_guide/context/):



```python title=main.py
import asyncpg
from cocoindex.connectors import postgres

PG_DB = coco.ContextKey[asyncpg.Pool]("text_embedding_db")

@coco.lifespan
async def coco_lifespan(builder: coco.EnvironmentBuilder) -&amp;gt; AsyncIterator[None]:
    async with asyncpg.create_pool(DATABASE_URL) as pool:
        builder.provide(PG_DB, pool)
        builder.provide(EMBEDDER, SentenceTransformerEmbedder(EMBED_MODEL))
        yield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;detect_change=True&lt;/code&gt; on &lt;code&gt;EMBEDDER&lt;/code&gt; tells CocoIndex to watch the embedder itself. If you change &lt;code&gt;EMBED_MODEL&lt;/code&gt;, the next run embeds every chunk again with the new model.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: chunk and embed
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;process_chunk&lt;/code&gt; embeds one chunk and declares its row. &lt;code&gt;process_file&lt;/code&gt; reads a file, splits it into chunks, and runs &lt;code&gt;process_chunk&lt;/code&gt; on each one. &lt;code&gt;memo=True&lt;/code&gt; turns on &lt;a href="https://cocoindex.io/docs/advanced_topics/memoization_keys/" rel="noopener noreferrer"&gt;memoization&lt;/a&gt;: CocoIndex caches the result, and when a file's content and the function's code are unchanged, it skips the file on the next run.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
from cocoindex.ops.text import RecursiveSplitter&lt;br&gt;
from cocoindex.resources.chunk import Chunk&lt;br&gt;
from cocoindex.resources.file import FileLike&lt;br&gt;
from cocoindex.resources.id import IdGenerator&lt;/p&gt;

&lt;p&gt;_splitter = RecursiveSplitter()&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def process_chunk(&lt;br&gt;
    chunk: Chunk,&lt;br&gt;
    filename: pathlib.PurePath,&lt;br&gt;
    id_gen: IdGenerator,&lt;br&gt;
    table: postgres.TableTarget[DocEmbedding],&lt;br&gt;
) -&amp;gt; None:&lt;br&gt;
    table.declare_row(&lt;br&gt;
        row=DocEmbedding(&lt;br&gt;
            id=await id_gen.next_id(chunk.text),&lt;br&gt;
            filename=str(filename),&lt;br&gt;
            chunk_start=chunk.start.char_offset,&lt;br&gt;
            chunk_end=chunk.end.char_offset,&lt;br&gt;
            text=chunk.text,&lt;br&gt;
            embedding=await coco.use_context(EMBEDDER).embed(chunk.text),&lt;br&gt;
        ),&lt;br&gt;
    )&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn(memo=True)&lt;br&gt;
async def process_file(&lt;br&gt;
    file: FileLike,&lt;br&gt;
    table: postgres.TableTarget[DocEmbedding],&lt;br&gt;
) -&amp;gt; None:&lt;br&gt;
    text = await file.read_text()&lt;br&gt;
    chunks = _splitter.split(text, chunk_size=2000, chunk_overlap=500, language="markdown")&lt;br&gt;
    id_gen = IdGenerator()&lt;br&gt;
    await coco.map(process_chunk, chunks, file.file_path.path, id_gen, table)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


&amp;gt; **Match chunk_size to your model.** `chunk_size=2000` comes from the upstream example. With `all-MiniLM-L6-v2`, most of those chunks are longer than 256 tokens (the median is 433 tokens in the sample files), so the model reads only the start of each one. A smaller `chunk_size` keeps more of each chunk inside the limit: at 1,000 bytes about 3 in 4 chunks of the sample files fit, and at 600 bytes almost all do. A model with a longer input limit can use larger chunks.

`declare_row` does not write to the database at once. It declares a [target state](https://cocoindex.io/docs/programming_guide/target_state/), a row that should exist. CocoIndex compares the declared rows with the ones it declared on the last run and writes only the difference.

### Step 3: wire up the app

`app_main` sets up (mounts) the Postgres table, adds a vector index so Postgres can find the closest vectors quickly, walks the folder of Markdown files, and runs one `process_file` per file:



```python title=main.py
from cocoindex.connectors import localfs
from cocoindex.resources.file import PatternFilePathMatcher

@coco.fn
async def app_main(sourcedir: pathlib.Path) -&amp;gt; None:
    table = await postgres.mount_table_target(
        PG_DB,
        table_name="doc_embeddings",
        table_schema=await postgres.TableSchema.from_class(DocEmbedding, primary_key=["id"]),
        pg_schema_name="coco_examples",
    )
    table.declare_vector_index(column="embedding")

    files = localfs.walk_dir(
        sourcedir,
        recursive=True,
        path_matcher=PatternFilePathMatcher(included_patterns=["**/*.md"]),
        live=True,
    )
    await coco.mount_each(process_file, files.items(), table)

app = coco.App(
    coco.AppConfig(name="TextEmbeddingV1"),
    app_main,
    sourcedir=pathlib.Path("./markdown_files"),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;coco.mount_each&lt;/code&gt; gives each file its own &lt;a href="https://cocoindex.io/docs/programming_guide/processing_component/" rel="noopener noreferrer"&gt;processing component&lt;/a&gt;: a unit of work that CocoIndex runs, tracks and cleans up on its own. That is what lets CocoIndex update one file without touching the others.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: build the index
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cocoindex update main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first run embeds all 106 chunks of the sample files. Add &lt;code&gt;-L&lt;/code&gt; (&lt;code&gt;cocoindex update -L main&lt;/code&gt;) to keep watching the folder and update the index as files change (&lt;a href="https://cocoindex.io/docs/programming_guide/live_mode/" rel="noopener noreferrer"&gt;live mode&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  Query the index
&lt;/h2&gt;

&lt;p&gt;Search mirrors indexing: embed the question with the same model, then ask Postgres for the nearest vectors. In &lt;code&gt;main.py&lt;/code&gt;, the pool is created with &lt;code&gt;asyncpg.create_pool(DATABASE_URL, init=register_vector)&lt;/code&gt; (from &lt;code&gt;pgvector.asyncpg&lt;/code&gt;), so asyncpg can send a vector as a query parameter. A different model puts text in a different space, so its vectors cannot be compared with the stored ones.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
async def query_once(pool, embedder, query: str, *, top_k: int = 5) -&amp;gt; None:&lt;br&gt;
    query_vec = await embedder.embed(query)&lt;br&gt;
    async with pool.acquire() as conn:&lt;br&gt;
        rows = await conn.fetch(&lt;br&gt;
            """&lt;br&gt;
            SELECT filename, text, embedding &amp;lt;=&amp;gt; $1 AS distance&lt;br&gt;
            FROM "coco_examples"."doc_embeddings"&lt;br&gt;
            ORDER BY distance ASC&lt;br&gt;
            LIMIT $2&lt;br&gt;
            """,&lt;br&gt;
            query_vec,&lt;br&gt;
            top_k,&lt;br&gt;
        )&lt;br&gt;
    for r in rows:&lt;br&gt;
        print(f"[{1.0 - float(r['distance']):.3f}] {r['filename']}")&lt;br&gt;
        print(f"    {r['text']}")&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Run it with a question in plain language:



```sh
python main.py "what is self-attention?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;$1 LIMIT 5. The top 5 of 106 chunks by score, with the chunk's first line and where it is: 0.494, #### Attention Visualizations, 1706.03762v7.md 45126 to 46144; 0.445, ### 3.2.3 Applications of Attention in our Model, 13016 to 14977; 0.419, ### 4 Why Self-Attention, 17982 to 19559; 0.343, ## 2 Background, 4905 to 6513; 0.329, ### 3.2.1 Scaled Dot-Product Attention, 9743 to 11378. Score = 1 minus distance."} title={"A search"} width={960} height={452} detailScale={1.4} minWidth={840} /&amp;gt;&lt;/p&gt;

&lt;p&gt;All 5 hits come from the Transformer paper, and none of them comes from the BERT paper or the JSON spec. The section 4 row from Step 1 is the third hit. We computed this ranking with the same model; &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; gives the same order, because distance is 1 minus similarity. With the vector index, Postgres may return a slightly different top 5, because that index finds nearest neighbors approximately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A re-run redoes only what changed
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;cocoindex update main&lt;/code&gt; again and CocoIndex does only the work that the change needs:&lt;/p&gt;

&lt;p&gt;2 mechanisms make this work. &lt;code&gt;memo=True&lt;/code&gt; on &lt;code&gt;process_file&lt;/code&gt; skips files that did not change. The embedder's &lt;code&gt;embed()&lt;/code&gt; is memoized too, so in an edited file only the chunks with new text go to the model. On the write side, each row's &lt;code&gt;id&lt;/code&gt; comes from &lt;code&gt;IdGenerator.next_id(chunk.text)&lt;/code&gt;, so a chunk with the same text keeps the same id across runs. Rows for chunks that are gone are deleted, new chunks are inserted, and rows whose text moved get their new offsets without a new embedding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have the core of semantic search: chunk, embed, store, and query by nearest neighbor. The same pipeline can read from &lt;a href="https://cocoindex.io/blogs/s3-incremental-etl/" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt;, &lt;a href="https://cocoindex.io/docs/connectors/azure_blob/" rel="noopener noreferrer"&gt;Azure Blob&lt;/a&gt; or Google Drive instead of a local folder, write to a vector database such as Qdrant instead of Postgres, or pass the passages it finds to an LLM for RAG (retrieval-augmented generation: the LLM answers from the passages you retrieved). Because the pipeline is declarative and incremental, none of that changes how the index stays fresh.&lt;/p&gt;

&lt;p&gt;For code instead of Markdown, &lt;a href="https://cocoindex.io/docs/examples/index-codebase/" rel="noopener noreferrer"&gt;Index your codebase&lt;/a&gt; uses the same flow with syntax-aware chunking.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>🎉 CocoIndex hits 1,000 stars – powering real-time data for AI</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Tue, 13 May 2025 04:21:52 +0000</pubDate>
      <link>https://dev.to/cocoindex/story-of-cocoindex-at-1k-stars-4ohl</link>
      <guid>https://dev.to/cocoindex/story-of-cocoindex-at-1k-stars-4ohl</guid>
      <description>&lt;p&gt;We’ve been building &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex&lt;/a&gt;, an ultra performant real-time data transformation framework for AI, and the response from the developer and data engineering community has been incredible. This week, CocoIndex officially surpassed 1,000 stars on GitHub! &lt;/p&gt;

&lt;p&gt;🎉 A huge thank you to everyone who has starred, forked, contributed, or shared the project — your support is helping shape the future of open-source AI data infrastructure. ❤️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CocoIndex&lt;/strong&gt; is a high-performance, real-time data transformation framework designed for modern AI workflows. Built on a blazing-fast Rust engine, CocoIndex simplifies preparing fresh, continuously updated data for AI applications — including embedding generation, knowledge graph construction, and other complex data transformations. It enables developers to build real-time data pipelines that go far beyond the limitations of traditional SQL-based systems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0yd6j0f4i3mtuqh9bef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0yd6j0f4i3mtuqh9bef.png" alt="CocoIndex Feature" width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The philosophy is to have the framework handle the source updates, and having developers only worry about defining a series of data transformation, inspired by spreadsheet.&lt;/p&gt;

&lt;p&gt;Here's a bit history about our open source journey:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8b2x9yrnq380pcu57dtx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8b2x9yrnq380pcu57dtx.png" alt="Star history" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Flow Programming
&lt;/h2&gt;

&lt;p&gt;Unlike conventional workflow orchestration frameworks—where data is typically treated as an opaque payload—CocoIndex elevates data and data transformations to first-class citizens within its architecture. Built around the &lt;a href="https://en.wikipedia.org/wiki/Dataflow_programming" rel="noopener noreferrer"&gt;Dataflow programming model&lt;/a&gt;, CocoIndex enforces a design where each transformation is a pure, stateless function that produces a new output field exclusively from its input fields, without relying on hidden states or mutating existing data.&lt;/p&gt;

&lt;p&gt;This approach guarantees deterministic and reproducible transformations that can be fully observed and audited. Every intermediate state of the data, both before and after each transformation, is completely visible and traceable, enabling automatic data lineage tracking. This lineage provides deep insights into how data flows through the pipeline, making it easier to debug, troubleshoot, and optimize real-time data pipelines.&lt;/p&gt;

&lt;p&gt;By embracing this model, CocoIndex empowers developers to build transparent, incremental, and highly performant AI data pipelines where data freshness and accuracy are maintained effortlessly. This visibility is critical for AI applications that depend on reliable and up-to-date information, such as embedding generation, knowledge graph construction, and complex data enrichment workflows.&lt;/p&gt;

&lt;p&gt;For example this is a kind of a data flow: Parse files -&amp;gt; Data Mapping -&amp;gt; Data Extraction -&amp;gt; Knowledge Graph&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60ijubna16akuii3gai8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60ijubna16akuii3gai8.png" alt="DataFlow" width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Particularly&lt;/strong&gt;, users don't define data operations like creation, update, or deletion. Rather, they define something like -&lt;br&gt;
for a set of source data, this is the transformation or formula. The framework takes care of the data operations such as when to create, update, or delete.&lt;/p&gt;

&lt;p&gt;With operations like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ingest
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flow_builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_source&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 

&lt;span class="c1"&gt;# transform
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="c1"&gt;# collect data
&lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="c1"&gt;# export to db, vector db, graph db ...
&lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Data Freshness
&lt;/h2&gt;

&lt;p&gt;As a cutting-edge data framework, CocoIndex significantly enhances data freshness by leveraging incremental processing—a core feature that ensures only changed data is processed and updated, enabling faster, more efficient real-time data pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2n18nwlivk9zfponej3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2n18nwlivk9zfponej3.gif" alt="Incremental Processing" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The framework takes care of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change data capture&lt;/li&gt;
&lt;li&gt;Figuring out what exactly needs to be updated, and only updating that without having to recompute everything throughout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This makes it fast to reflect any source updates to the target store. If you have concerns with surfacing stale data to AI agents and are spending lots of effort working on infrastructure pieces to optimize latency, the framework actually handles it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-in modules and custom modules
&lt;/h2&gt;

&lt;p&gt;At CocoIndex, we recognize that data preparation is highly use-case specific, and there is no one-size-fits-all solution for every AI workflow. With rapid advancements in AI models and data technologies, selecting the best tools involves balancing multiple factors such as data quality, performance, and cost efficiency.&lt;/p&gt;

&lt;p&gt;The data infrastructure ecosystem offers a wide variety of specialized tools — including parsers, embedding generation models, vector databases, graph databases, and more — each optimized for different goals and scenarios.&lt;/p&gt;

&lt;p&gt;Instead of building everything from scratch, CocoIndex adopts a modular, composition-based approach. We provide native plugin support that enables seamless integration with your preferred data tools and services. By standardizing plugin interfaces, CocoIndex allows developers to easily plug in, swap, or upgrade components like building blocks, just like LEGO. This flexibility empowers you to tailor your AI data pipelines for maximum efficiency and adaptability.&lt;/p&gt;

&lt;p&gt;And we can focus on what we're best at - building the best incremental pipeline and compute engine in the AI era.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling
&lt;/h2&gt;

&lt;p&gt;In addition to standard statistics and reports with CocoIndex, we are building a product called CocoInsight. It has zero pipeline data retention and connects to your on-premise CocoIndex server for pipeline insights. This makes data directly visible and easy to develop ETL pipelines.  &lt;/p&gt;

&lt;p&gt;Especially, as we mentioned earlier, CocoIndex is heavily influenced by spreadsheets. This is directly reflected in CocoInsight, as you can see all the data in the spreadsheet and how it looks after each transformation.&lt;/p&gt;

&lt;p&gt;This is an example of mapping:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegdallh3hma7e5f4r1b8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegdallh3hma7e5f4r1b8.png" alt="Mapping" width="799" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition to the data insights for data transformation, we also aim to provide tooling to make it directly visible to understand the LLM transformations, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand and trouble shooting the chunking, if there's any, and debug why a chunk is showing from search results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaibd0ebfp34q2v7ms3x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaibd0ebfp34q2v7ms3x.png" alt="chunking" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand the relation extraction for Graphs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzelqki8jquyfzb0bwej4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzelqki8jquyfzb0bwej4.png" alt="Relationship" width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data should be transparent, especially for ETL frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support us
&lt;/h2&gt;

&lt;p&gt;We are constantly improving, and more features and examples are coming soon. If you love this article, please drop us a star ⭐ at &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; to help us grow. You can also find us on &lt;a href="https://discord.com/invite/zpA9S2DR7s" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;, I try to be there 24/7 😊 Looking forward to discussing CocoIndex or any topics in Data/AI infra with you!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Real-Time Knowledge Graph for Product recommendation with LLM taxonomy extraction</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Thu, 08 May 2025 05:22:57 +0000</pubDate>
      <link>https://dev.to/cocoindex/real-time-knowledge-graph-for-product-insights-with-llm-taxonomy-understanding-21f4</link>
      <guid>https://dev.to/cocoindex/real-time-knowledge-graph-for-product-insights-with-llm-taxonomy-understanding-21f4</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/product-recommendation/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A pile of product listings has recommendations hiding in plain sight: a &lt;em&gt;pen&lt;/em&gt; pairs with &lt;em&gt;ink refills&lt;/em&gt; and a &lt;em&gt;notebook&lt;/em&gt;; a &lt;em&gt;monitor&lt;/em&gt; pairs with a &lt;em&gt;stand&lt;/em&gt; and an &lt;em&gt;HDMI cable&lt;/em&gt;. That knowledge is locked in the prose of each listing. The recommendation engine uses these relationships: an LLM pulls the knowledge out, and a Neo4j graph connects it.&lt;/p&gt;

&lt;p&gt;For each product, CocoIndex v1 asks an LLM two things: what the product &lt;em&gt;is&lt;/em&gt; (its taxonomy), and what a buyer might &lt;em&gt;also need&lt;/em&gt; (its complementary taxonomy). Those labels become nodes and edges in a Neo4j graph, and the recommendation falls out of the graph: a product whose "also need" matches another product's "is" is the thing to cross-sell.&lt;/p&gt;

&lt;p&gt;The full code is in the &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/product_recommendation" rel="noopener noreferrer"&gt;CocoIndex v1 repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model
&lt;/h2&gt;

&lt;p&gt;Two node types and two edge types are enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Product&lt;/code&gt;&lt;/strong&gt; nodes, one per listing, keyed by &lt;code&gt;id&lt;/code&gt; (the filename), carrying &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;price&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Taxonomy&lt;/code&gt;&lt;/strong&gt; nodes, one per distinct label (&lt;code&gt;gel pen&lt;/code&gt;, &lt;code&gt;notebook&lt;/code&gt;, &lt;code&gt;ink refill&lt;/code&gt;), keyed by &lt;code&gt;value&lt;/code&gt; and &lt;strong&gt;shared&lt;/strong&gt; across products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PRODUCT_TAXONOMY&lt;/code&gt;&lt;/strong&gt; edges, &lt;code&gt;Product → Taxonomy&lt;/code&gt;: what the product is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PRODUCT_COMPLEMENTARY_TAXONOMY&lt;/code&gt;&lt;/strong&gt; edges, &lt;code&gt;Product → Taxonomy&lt;/code&gt;: what pairs with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nodes are plain dataclasses:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
from dataclasses import dataclass&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class Product:&lt;br&gt;
    id: str      # primary key (the filename stem)&lt;br&gt;
    title: str&lt;br&gt;
    price: float&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class Taxonomy:&lt;br&gt;
    value: str   # primary key (the taxonomy label)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## The flow

Because taxonomy labels are shared across products, the pipeline runs in two phases: per-product extraction declares each `Product` node and carries its labels forward, then a single graph-building pass declares the deduplicated `Taxonomy` nodes and all the edges.

## Connect Neo4j

The Neo4j connection is provided once in the app lifespan and read anywhere with a [`ContextKey`](https://cocoindex.io/docs/programming_guide/core_concepts/). The LLM model name is a context value too, declared with `detect_change=True` so swapping models re-extracts everything against the new one:



```python title=main.py
import cocoindex as coco
from cocoindex.connectors import neo4j

KG_DB = coco.ContextKey[neo4j.ConnectionFactory]("kg_db")
LLM_MODEL = coco.ContextKey[str]("llm_model", detect_change=True)

@coco.lifespan
async def coco_lifespan(builder: coco.EnvironmentBuilder) -&amp;gt; AsyncIterator[None]:
    builder.provide(
        KG_DB,
        neo4j.ConnectionFactory(
            uri=os.environ.get("NEO4J_URI", "bolt://localhost:7687"),
            auth=(os.environ["NEO4J_USER"], os.environ["NEO4J_PASSWORD"]),
        ),
    )
    builder.provide(LLM_MODEL, os.environ.get("LLM_MODEL", "openai/gpt-4.1"))
    yield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Extract the taxonomy
&lt;/h2&gt;

&lt;p&gt;The extraction schema is the whole prompt. A Pydantic model asks for two lists, and the field descriptions tell the model exactly what a good label looks like: a concise noun, US English, specific rather than broad (&lt;code&gt;gel pen&lt;/code&gt;, not &lt;code&gt;office supplies&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
import pydantic&lt;/p&gt;

&lt;p&gt;class ProductTaxonomy(pydantic.BaseModel):&lt;br&gt;
    name: str = pydantic.Field(&lt;br&gt;
        description="A concise noun for the product's core function, no branding "&lt;br&gt;
        "or style. Common US English, lowercase. Prefer specific ('pen') over "&lt;br&gt;
        "broad ('office supplies')."&lt;br&gt;
    )&lt;/p&gt;

&lt;p&gt;class ProductTaxonomyInfo(pydantic.BaseModel):&lt;br&gt;
    taxonomies: list[ProductTaxonomy] = pydantic.Field(&lt;br&gt;
        description="Taxonomies describing what this product is."&lt;br&gt;
    )&lt;br&gt;
    complementary_taxonomies: list[ProductTaxonomy] = pydantic.Field(&lt;br&gt;
        description="Taxonomies for complementary products a buyer might also need."&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Extraction is one CocoIndex function that calls the model through [instructor](https://github.com/instructor-ai/instructor) over [LiteLLM](https://cocoindex.io/docs/ops/litellm/), forcing the response to match the schema. `memo=True` caches the result by content, so an unchanged product is never sent to the model twice:



```python title=main.py
@coco.fn(memo=True)
async def extract_taxonomy(detail: str) -&amp;gt; ProductTaxonomyInfo:
    client = instructor.from_litellm(litellm.acompletion, mode=instructor.Mode.JSON)
    result = await client.chat.completions.create(
        model=coco.use_context(LLM_MODEL),
        response_model=ProductTaxonomyInfo,
        messages=[
            {"role": "system", "content": TAXONOMY_PROMPT},
            {"role": "user", "content": detail},
        ],
    )
    return ProductTaxonomyInfo.model_validate(result.model_dump())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the gel pen, the model returns &lt;code&gt;gel pen&lt;/code&gt; as the taxonomy, and &lt;code&gt;notebook&lt;/code&gt;, &lt;code&gt;pen refill&lt;/code&gt;, and the like as complementary taxonomies:&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 1: one Product node per file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;process_file&lt;/code&gt; handles a single product: read the JSON, declare its &lt;code&gt;Product&lt;/code&gt; node into the graph, extract the taxonomy, and return the labels for phase two. &lt;code&gt;declare_record&lt;/code&gt; writes one node; CocoIndex reconciles it into Neo4j by the primary key:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn(memo=True)&lt;br&gt;
async def process_file(&lt;br&gt;
    file: FileLike,&lt;br&gt;
    product_table: neo4j.TableTarget[Product],&lt;br&gt;
) -&amp;gt; ProductTaxonomies:&lt;br&gt;
    raw = json.loads(await file.read_text())&lt;br&gt;
    product_id = file.file_path.path.name.removesuffix(".json")&lt;br&gt;
    price = float(str(raw["price"]).lstrip("$").replace(",", ""))&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_table.declare_record(row=Product(id=product_id, title=raw["title"], price=price))

info = await extract_taxonomy(PRODUCT_TEMPLATE.render(**raw))
return ProductTaxonomies(
    product_id=product_id,
    taxonomies=[t.name for t in info.taxonomies],
    complementary=[t.name for t in info.complementary_taxonomies],
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Each declared `Product` becomes a node with `id` as its key and every field as a property:

## Phase 2: shared Taxonomy nodes and the edges

Taxonomy labels repeat across products, so one pass owns them. It gathers every label, declares the `Taxonomy` nodes, and declares the edges. Because a node is keyed by `value`, declaring `gel pen` from ten different products still produces exactly one node: CocoIndex deduplicates by the primary key.

`declare_relation` connects a product to a taxonomy by their keys. Two relation targets, one per edge type, share the same `Product` and `Taxonomy` node tables:



```python title=main.py
@coco.fn
async def build_graph(
    products: list[ProductTaxonomies],
    taxonomy_table: neo4j.TableTarget[Taxonomy],
    product_taxonomy_rel: neo4j.RelationTarget[Any],
    complementary_rel: neo4j.RelationTarget[Any],
) -&amp;gt; None:
    labels = {t for p in products for t in (*p.taxonomies, *p.complementary)}
    for value in labels:
        taxonomy_table.declare_record(row=Taxonomy(value=value))

    for p in products:
        for t in set(p.taxonomies):
            product_taxonomy_rel.declare_relation(from_id=p.product_id, to_id=t)
        for t in set(p.complementary):
            complementary_rel.declare_relation(from_id=p.product_id, to_id=t)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;PRODUCT_TAXONOMY&lt;/code&gt; edges link each product to what it is:&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;PRODUCT_COMPLEMENTARY_TAXONOMY&lt;/code&gt; edges add what pairs with it. These are the edges that cross between products and make recommendations possible:&lt;/p&gt;
&lt;h2&gt;
  
  
  Wire up the app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app_main&lt;/code&gt; mounts the two node tables and the two relation targets, walks the product folder, fans out one &lt;code&gt;process_file&lt;/code&gt; per product, then runs the single &lt;code&gt;build_graph&lt;/code&gt; pass over the collected labels:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def app_main(sourcedir: pathlib.Path) -&amp;gt; None:&lt;br&gt;
    product_table = await neo4j.mount_table_target(&lt;br&gt;
        KG_DB, "Product",&lt;br&gt;
        await neo4j.TableSchema.from_class(Product, primary_key="id"),&lt;br&gt;
        primary_key="id",&lt;br&gt;
    )&lt;br&gt;
    taxonomy_table = await neo4j.mount_table_target(&lt;br&gt;
        KG_DB, "Taxonomy",&lt;br&gt;
        await neo4j.TableSchema.from_class(Taxonomy, primary_key="value"),&lt;br&gt;
        primary_key="value",&lt;br&gt;
    )&lt;br&gt;
    product_taxonomy_rel = await neo4j.mount_relation_target(&lt;br&gt;
        KG_DB, "PRODUCT_TAXONOMY", product_table, taxonomy_table&lt;br&gt;
    )&lt;br&gt;
    complementary_rel = await neo4j.mount_relation_target(&lt;br&gt;
        KG_DB, "PRODUCT_COMPLEMENTARY_TAXONOMY", product_table, taxonomy_table&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files = localfs.walk_dir(
    sourcedir, recursive=True,
    path_matcher=PatternFilePathMatcher(included_patterns=["**/*.json"]),
)
products = list(await asyncio.gather(*[
    coco.use_mount(coco.component_subpath("file", key), process_file, file, product_table)
    async for key, file in files.items()
]))

await coco.mount(
    coco.component_subpath("build_graph"),
    build_graph, products, taxonomy_table, product_taxonomy_rel, complementary_rel,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;app = coco.App(coco.AppConfig(name="ProductRecommendation"), app_main,&lt;br&gt;
               sourcedir=pathlib.Path("./products"))&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


## Run it

Start Neo4j, install the example, and build the graph. The example ships a `products/` folder of sample listings:



```sh
docker run -d -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/cocoindex neo4j:5.26-community
pip install "cocoindex[neo4j]" instructor litellm pydantic jinja2
cocoindex update main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On the nine sample products that is nine &lt;code&gt;Product&lt;/code&gt; nodes, around forty &lt;code&gt;Taxonomy&lt;/code&gt; nodes, and both edge types wired up. Editing one product re-extracts only that product, then the graph diffs: new nodes and edges are added, and ones no longer supported anywhere are removed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Query the recommendations
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="http://localhost:7474" rel="noopener noreferrer"&gt;Neo4j Browser&lt;/a&gt; (&lt;code&gt;neo4j&lt;/code&gt; / &lt;code&gt;cocoindex&lt;/code&gt;) and let the graph do the recommending. To find products to pair with a gel pen, walk from the pen's complementary taxonomy to any product that &lt;em&gt;is&lt;/em&gt; that taxonomy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Taxonomy&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;value:&lt;/span&gt; &lt;span class="s2"&gt;"gel pen"&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PRODUCT_TAXONOMY&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PRODUCT_COMPLEMENTARY_TAXONOMY&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;need:&lt;/span&gt;&lt;span class="n"&gt;Taxonomy&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;rec:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PRODUCT_TAXONOMY&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;rec.title&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the sample data, recommending for a pen surfaces the notepad and the multipurpose paper, the cross-sell you would want. To see the whole graph, &lt;code&gt;MATCH p=()--&amp;gt;() RETURN p&lt;/code&gt;:&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Build Real-Time Knowledge Graph For Documents with LLM</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Thu, 01 May 2025 01:36:50 +0000</pubDate>
      <link>https://dev.to/cocoindex/build-real-time-knowledge-graph-for-documents-with-llm-256h</link>
      <guid>https://dev.to/cocoindex/build-real-time-knowledge-graph-for-documents-with-llm-256h</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/knowledge-graph-for-docs/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Documentation is a web of concepts pretending to be a list of files. Every page asserts relationships ("CocoIndex supports incremental processing"), but they are locked in prose. This post turns a folder of Markdown documents (CocoIndex's own docs) into a Neo4j knowledge graph: an LLM reads each document and extracts the relationships between the concepts it talks about, and CocoIndex reconciles them into the graph incrementally as documents change.&lt;/p&gt;

&lt;p&gt;We generate two kinds of relationships:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Relationships between subjects and objects. E.g., "CocoIndex supports &lt;a href="https://cocoindex.io/blogs/incremental-processing" rel="noopener noreferrer"&gt;Incremental Processing&lt;/a&gt;".&lt;/li&gt;
&lt;li&gt;Mentions of entities in a document. E.g., &lt;code&gt;basics.md&lt;/code&gt; mentions &lt;code&gt;CocoIndex&lt;/code&gt; and &lt;code&gt;Incremental Processing&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The end result is a small, well-defined graph schema: two node labels (&lt;code&gt;Document&lt;/code&gt; and &lt;code&gt;Entity&lt;/code&gt;) and two relationship types (&lt;code&gt;RELATIONSHIP&lt;/code&gt; between entities, and &lt;code&gt;MENTION&lt;/code&gt; connecting a document to the entities it references). Everything the LLM extracts lands in one of those four shapes, which is what keeps the graph queryable rather than a loose pile of triples.&lt;/p&gt;

&lt;p&gt;The source code is available at &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/docs_to_knowledge_graph" rel="noopener noreferrer"&gt;CocoIndex Examples - docs_to_knowledge_graph&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://cocoindex.io/docs/connectors/neo4j" rel="noopener noreferrer"&gt;Install Neo4j&lt;/a&gt;, a graph database. A docker one-liner is in the Run it section below.&lt;/li&gt;
&lt;li&gt;  An LLM API key. The example defaults to OpenAI, and the model is a &lt;a href="https://docs.litellm.ai/docs/providers" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt; id, so you can set &lt;code&gt;LLM_MODEL=ollama/llama3.2&lt;/code&gt; to run the extraction locally instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The graph schema
&lt;/h2&gt;

&lt;p&gt;All nodes for Neo4j need two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Label: the type of the node. E.g., &lt;code&gt;Document&lt;/code&gt;, &lt;code&gt;Entity&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Primary key: the field that uniquely identifies the node. E.g., &lt;code&gt;filename&lt;/code&gt; for &lt;code&gt;Document&lt;/code&gt; nodes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CocoIndex uses the primary key to match and deduplicate nodes: declaring the same key twice produces one node. In CocoIndex v1 the schema is plain dataclasses:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
from dataclasses import dataclass&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class Document:&lt;br&gt;
    filename: str  # primary key&lt;br&gt;
    title: str&lt;br&gt;
    summary: str&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class Entity:&lt;br&gt;
    value: str  # primary key — the concept name&lt;/p&gt;

&lt;p&gt;@dataclass&lt;br&gt;
class Relationship:&lt;br&gt;
    """RELATIONSHIP edge payload. &lt;code&gt;id&lt;/code&gt; is a stable hash of the triple so the&lt;br&gt;
    same (subject, predicate, object) always maps to a single edge; the&lt;br&gt;
    &lt;code&gt;predicate&lt;/code&gt; is stored as an edge property."""&lt;br&gt;
    id: int&lt;br&gt;
    predicate: str&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`MENTION` carries no payload, so it needs no dataclass at all: the Neo4j connector derives its identity from the `(document, entity)` endpoints, one edge per pair.

## The flow

Entities are shared across documents: the same concept, say `CocoIndex`, shows up in triples from many files, and every one of those references must collapse onto a single `Entity` node. So the pipeline runs in two phases. Phase 1 processes each document independently: it declares the `Document` node and extracts the relationship triples. Phase 2 is a single pass over all the triples: it owns the deduplicated `Entity` nodes and declares the `RELATIONSHIP` and `MENTION` edges.

Because phase 1 runs one [component](https://cocoindex.io/docs/programming_guide/processing_component) per file, editing one document re-extracts only that document. The graph pass then diffs: nodes and edges that are no longer supported by any document are removed automatically.

## Connect Neo4j

The Neo4j connection is provided once in the app lifespan and read anywhere with a [`ContextKey`](https://cocoindex.io/docs/programming_guide/core_concepts/). The LLM model name is a context value too, declared with `detect_change=True` so swapping models re-extracts everything against the new one:



```python title=main.py
import cocoindex as coco
from cocoindex.connectors import localfs, neo4j

KG_DB = coco.ContextKey[neo4j.ConnectionFactory]("kg_db")
LLM_MODEL = coco.ContextKey[str]("llm_model", detect_change=True)

@coco.lifespan
async def coco_lifespan(builder: coco.EnvironmentBuilder) -&amp;gt; AsyncIterator[None]:
    builder.provide(
        KG_DB,
        neo4j.ConnectionFactory(
            uri=os.environ.get("NEO4J_URI", "bolt://localhost:7687"),
            auth=(
                os.environ.get("NEO4J_USER", "neo4j"),
                os.environ.get("NEO4J_PASSWORD", "cocoindex"),
            ),
            database=os.environ.get("NEO4J_DATABASE", "neo4j"),
        ),
    )
    builder.provide(LLM_MODEL, os.environ.get("LLM_MODEL", "openai/gpt-5-mini"))
    yield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Summarize each document
&lt;/h2&gt;

&lt;p&gt;The extraction schemas are Pydantic models, and the field descriptions are part of the prompt. For the summary we ask for a title and one paragraph:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
import pydantic&lt;/p&gt;

&lt;p&gt;class DocumentSummary(pydantic.BaseModel):&lt;br&gt;
    title: str = pydantic.Field(description="A concise title for the document.")&lt;br&gt;
    summary: str = pydantic.Field(&lt;br&gt;
        description="A one-paragraph summary of what the document covers."&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Extraction is one CocoIndex function that calls the model through [instructor](https://github.com/instructor-ai/instructor) over [LiteLLM](https://cocoindex.io/docs/ops/litellm/), forcing the response to match the schema. `memo=True` [memoizes](https://cocoindex.io/docs/advanced_topics/memoization_keys) the result by content, so an unchanged document is never sent to the model twice:



```python title=main.py
@coco.fn(memo=True)
async def extract_summary(content: str) -&amp;gt; DocumentSummary:
    client = instructor.from_litellm(litellm.acompletion, mode=instructor.Mode.JSON)
    result = await client.chat.completions.create(
        model=coco.use_context(LLM_MODEL),
        response_model=DocumentSummary,
        messages=[
            {"role": "system", "content": SUMMARY_PROMPT},
            {"role": "user", "content": content},
        ],
    )
    return DocumentSummary.model_validate(result.model_dump())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For each document the model returns a typed summary:&lt;/p&gt;
&lt;h2&gt;
  
  
  Extract relationship triples
&lt;/h2&gt;

&lt;p&gt;A relationship is a &lt;code&gt;(subject, predicate, object)&lt;/code&gt; triple. The subject and object become &lt;code&gt;Entity&lt;/code&gt; nodes, and the predicate is the label on the edge between them:&lt;/p&gt;

&lt;p&gt;The Pydantic model mirrors that shape, and the system prompt steers the model toward concepts rather than code:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
class ExtractedRelationship(pydantic.BaseModel):&lt;br&gt;
    subject: str = pydantic.Field(&lt;br&gt;
        description="The concept the statement is about, e.g. 'CocoIndex'."&lt;br&gt;
    )&lt;br&gt;
    predicate: str = pydantic.Field(&lt;br&gt;
        description="How subject relates to object, e.g. 'supports'."&lt;br&gt;
    )&lt;br&gt;
    object: str = pydantic.Field(&lt;br&gt;
        description="The related concept, e.g. 'Incremental Processing'."&lt;br&gt;
    )&lt;/p&gt;

&lt;p&gt;class RelationshipList(pydantic.BaseModel):&lt;br&gt;
    relationships: list[ExtractedRelationship] = pydantic.Field(default_factory=list)&lt;/p&gt;

&lt;p&gt;RELATIONSHIP_PROMPT = """\&lt;br&gt;
You extract a concept knowledge graph from technical documentation.&lt;br&gt;
List the salient (subject, predicate, object) relationships between concepts.&lt;br&gt;
Focus on concepts and ignore code examples and implementation details.&lt;br&gt;
Use concise noun phrases for subjects and objects and a short verb phrase for&lt;br&gt;
the predicate. Return only relationships supported by the text.&lt;br&gt;
"""&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


The extraction function has the same shape as the summary one, memoized the same way:



```python title=main.py
@coco.fn(memo=True)
async def extract_relationships(content: str) -&amp;gt; list[Triple]:
    client = instructor.from_litellm(litellm.acompletion, mode=instructor.Mode.JSON)
    result = await client.chat.completions.create(
        model=coco.use_context(LLM_MODEL),
        response_model=RelationshipList,
        messages=[
            {"role": "system", "content": RELATIONSHIP_PROMPT},
            {"role": "user", "content": content},
        ],
    )
    validated = RelationshipList.model_validate(result.model_dump())
    return [Triple(r.subject, r.predicate, r.object) for r in validated.relationships]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On the CocoIndex docs, one file yields a few dozen triples:&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 1: one Document node per file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;process_file&lt;/code&gt; handles a single document: read the content, extract the summary, declare the &lt;code&gt;Document&lt;/code&gt; node into the graph, and return the triples for phase 2. &lt;code&gt;declare_record&lt;/code&gt; writes one node; CocoIndex reconciles it into Neo4j by the primary key:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn(memo=True)&lt;br&gt;
async def process_file(&lt;br&gt;
    file: localfs.File,&lt;br&gt;
    document_table: neo4j.TableTarget[Document],&lt;br&gt;
) -&amp;gt; DocTriples:&lt;br&gt;
    content = await file.read_text()&lt;br&gt;
    filename = file.file_path.path.as_posix()&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;summary = await extract_summary(content)
document_table.declare_record(
    row=Document(filename=filename, title=summary.title, summary=summary.summary)
)

triples = await extract_relationships(content)
return DocTriples(filename=filename, triples=triples)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


Each declared record becomes a `Document` node keyed by `filename`, with the title and summary carried across as node properties:

## Phase 2: build the concept graph

Phase 2 receives the triples from every document. Because `Entity` nodes are keyed by `value`, declaring `CocoIndex` from ten different documents still produces exactly one node: CocoIndex deduplicates by the primary key. The edges then fan out from that one node, which is exactly what you want when you later query "what mentions this concept?".

One pass gathers the distinct entities and mentions, then declares everything. `declare_relation` connects two nodes by their primary keys, and `generate_id` hashes each triple so the same fact always maps to the same edge, whether it is re-extracted or asserted again by another document:



```python title=main.py
@coco.fn
async def build_graph(
    docs: list[DocTriples],
    entity_table: neo4j.TableTarget[Entity],
    relationship_rel: neo4j.RelationTarget[Relationship],
    mention_rel: neo4j.RelationTarget[Any],
) -&amp;gt; None:
    entities: set[str] = set()
    mentions: set[tuple[str, str]] = set()  # (filename, entity value)

    for doc in docs:
        for t in doc.triples:
            entities.add(t.subject)
            entities.add(t.object)
            mentions.add((doc.filename, t.subject))
            mentions.add((doc.filename, t.object))

            rel_id = await generate_id((t.subject, t.predicate, t.object))
            relationship_rel.declare_relation(
                from_id=t.subject,
                to_id=t.object,
                record=Relationship(id=rel_id, predicate=t.predicate),
            )

    for value in entities:
        entity_table.declare_record(row=Entity(value=value))

    for filename, entity in mentions:
        mention_rel.declare_relation(from_id=filename, to_id=entity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;RELATIONSHIP&lt;/code&gt; edges connect &lt;code&gt;Entity&lt;/code&gt; nodes, with the predicate stored on the edge:&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MENTION&lt;/code&gt; edges record which document named which concept. For example, &lt;code&gt;basics.md&lt;/code&gt; has a sentence "CocoIndex supports Incremental Processing", so it mentions both &lt;code&gt;CocoIndex&lt;/code&gt; and &lt;code&gt;Incremental Processing&lt;/code&gt;:&lt;/p&gt;
&lt;h2&gt;
  
  
  Wire up the app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app_main&lt;/code&gt; mounts the two node tables and the two relation targets, walks the docs folder, fans out one &lt;code&gt;process_file&lt;/code&gt; per document, then runs the single &lt;code&gt;build_graph&lt;/code&gt; pass over the collected triples:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```python title=main.py&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/coco"&gt;@coco&lt;/a&gt;.fn&lt;br&gt;
async def app_main(sourcedir: pathlib.Path) -&amp;gt; None:&lt;br&gt;
    document_table = await neo4j.mount_table_target(&lt;br&gt;
        KG_DB,&lt;br&gt;
        "Document",&lt;br&gt;
        await neo4j.TableSchema.from_class(Document, primary_key="filename"),&lt;br&gt;
        primary_key="filename",&lt;br&gt;
    )&lt;br&gt;
    entity_table = await neo4j.mount_table_target(&lt;br&gt;
        KG_DB,&lt;br&gt;
        "Entity",&lt;br&gt;
        await neo4j.TableSchema.from_class(Entity, primary_key="value"),&lt;br&gt;
        primary_key="value",&lt;br&gt;
    )&lt;br&gt;
    relationship_rel = await neo4j.mount_relation_target(&lt;br&gt;
        KG_DB,&lt;br&gt;
        "RELATIONSHIP",&lt;br&gt;
        entity_table,&lt;br&gt;
        entity_table,&lt;br&gt;
        await neo4j.TableSchema.from_class(Relationship, primary_key="id"),&lt;br&gt;
        primary_key="id",&lt;br&gt;
    )&lt;br&gt;
    mention_rel = await neo4j.mount_relation_target(&lt;br&gt;
        KG_DB, "MENTION", document_table, entity_table&lt;br&gt;
    )&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files = localfs.walk_dir(
    sourcedir,
    recursive=True,
    path_matcher=PatternFilePathMatcher(included_patterns=["**/*.md", "**/*.mdx"]),
)
file_coros = []
async for path_key, file in files.items():
    file_coros.append(
        coco.use_mount(
            coco.component_subpath("file", path_key),
            process_file,
            file,
            document_table,
        )
    )
docs: list[DocTriples] = list(await asyncio.gather(*file_coros))

await coco.mount(
    coco.component_subpath("build_graph"),
    build_graph,
    docs,
    entity_table,
    relationship_rel,
    mention_rel,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;app = coco.App(&lt;br&gt;
    coco.AppConfig(name="DocsToKnowledgeGraph"),&lt;br&gt;
    app_main,&lt;br&gt;
    sourcedir=pathlib.Path("./markdown_files"),&lt;br&gt;
)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


`RELATIONSHIP` is mounted with the `Relationship` schema so each distinct triple, keyed by the hashed `id`, becomes its own edge. `MENTION` is mounted without a schema, so the connector keys it by the endpoints.

## Run it

Start Neo4j, install the example, and build the graph. The example ships a `markdown_files/` folder of sample docs so it runs out of the box:



```sh
docker run -d -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/cocoindex --name cocoindex-neo4j neo4j:5.26-community
cp .env.example .env     # set OPENAI_API_KEY (or LLM_MODEL=ollama/llama3.2 to run locally)
pip install -e .
cocoindex update main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To graph your own docs, drop &lt;code&gt;.md&lt;/code&gt; / &lt;code&gt;.mdx&lt;/code&gt; files into &lt;code&gt;markdown_files/&lt;/code&gt; (or point &lt;code&gt;sourcedir&lt;/code&gt; at your real docs folder) and re-run. Only changed documents are re-extracted; a no-change re-run makes zero LLM calls.&lt;/p&gt;
&lt;h2&gt;
  
  
  Browse the knowledge graph
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="http://localhost:7474" rel="noopener noreferrer"&gt;Neo4j Browser&lt;/a&gt; (username &lt;code&gt;neo4j&lt;/code&gt;, password &lt;code&gt;cocoindex&lt;/code&gt;) and run a Cypher query. To see how concepts relate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;RELATIONSHIP&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;a.value&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r.predicate&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find the concepts mentioned in the most documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d:&lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:MENTION&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;e.value&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or &lt;code&gt;MATCH p=()--&amp;gt;() RETURN p&lt;/code&gt; to see everything:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Near-duplicate entities.&lt;/strong&gt; The LLM will sometimes name the same concept two ways ("CocoIndex" vs "Cocoindex"). The &lt;a href="https://github.com/cocoindex-io/cocoindex/tree/main/examples/meeting_notes_graph_neo4j" rel="noopener noreferrer"&gt;meeting notes graph example&lt;/a&gt; adds an embedding plus LLM entity-resolution pass that collapses near-duplicates; it drops into this pipeline between the two phases.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>How to do incremental processing for ETL - by examples</title>
      <dc:creator>Linghua Jin</dc:creator>
      <pubDate>Tue, 08 Apr 2025 17:37:46 +0000</pubDate>
      <link>https://dev.to/cocoindex/how-to-do-incremental-processing-for-etl-by-examples-3ab6</link>
      <guid>https://dev.to/cocoindex/how-to-do-incremental-processing-for-etl-by-examples-3ab6</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is also on &lt;a href="https://cocoindex.io/blogs/incremental-processing/" rel="noopener noreferrer"&gt;cocoindex.io&lt;/a&gt;, with interactive figures. The code uses CocoIndex v1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Incremental processing is one of the core values provided by CocoIndex. &lt;br&gt;
In CocoIndex, users declare the transformation, and don't need to worry about the work to keep the index and source in sync.&lt;/p&gt;

&lt;p&gt;CocoIndex creates &amp;amp; maintains an index, and keeps the derived index up to date based on source updates, with minimal computation and changes. That makes it suitable for ETL/&lt;a href="https://cocoindex.io/blogs/index-codebase-v1/" rel="noopener noreferrer"&gt;RAG&lt;/a&gt; or any transformation tasks that stay low latency between source and index updates, and also minimizes the computation cost.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is incremental processing?
&lt;/h2&gt;

&lt;p&gt;Figuring out what exactly needs to be updated, and only updating that without having to recompute everything throughout.&lt;/p&gt;
&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;You don't really need to do anything special, just focus on defining the transformation needed.  &lt;/p&gt;

&lt;p&gt;CocoIndex automatically tracks the lineage of your data and maintains a cache of computation results. When you update your source data, CocoIndex will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify which parts of the data have changed&lt;/li&gt;
&lt;li&gt;Only recompute transformations for the changed data&lt;/li&gt;
&lt;li&gt;Reuse cached results for unchanged data&lt;/li&gt;
&lt;li&gt;Update the index with minimal changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And CocoIndex will handle the incremental processing for you. &lt;/p&gt;

&lt;p&gt;CocoIndex provides two modes to run the pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time update: Once triggered, CocoIndex updates the target data to reflect the version of source data up to the current moment.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cocoindex.io/docs/programming_guide/live_mode/" rel="noopener noreferrer"&gt;Live update&lt;/a&gt;: CocoIndex continuously reacts to changes of source data and updates the target data accordingly, based on various change capture mechanisms for the source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both modes run with incremental processing.  You can view more details in &lt;a href="https://cocoindex.io/docs/programming_guide/core_concepts/" rel="noopener noreferrer"&gt;Core Concepts&lt;/a&gt;.  &lt;/p&gt;
&lt;h2&gt;
  
  
  Who needs incremental processing?
&lt;/h2&gt;

&lt;p&gt;Many people may think incremental processing is only beneficial for large scale data. Thinking carefully, &lt;br&gt;
it really depends on the cost and requirement for data freshness.&lt;/p&gt;

&lt;p&gt;Google processes huge scale data, backed by a huge amount of resources.&lt;br&gt;
Your data scale is much less, but your resource provision is also much more limited.&lt;/p&gt;

&lt;p&gt;Incremental processing is needed under the following conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;High freshness requirement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most user-facing applications this is needed. e.g. users update their documents,&lt;br&gt;
and it's unexpected if they see stale information in search results.&lt;/p&gt;

&lt;p&gt;If the search result is fed into an AI agent, it may mean an unexpected response to users (i.e. an LLM generates output based on inaccurate information). &lt;br&gt;
It's more dangerous and users may even take the unexpected response without noticing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transformation cost is significantly higher than retrieval itself&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, say T is your most acceptable staleness. If you don't want to recompute the whole thing repeatedly every cycle of T, &lt;br&gt;
you will need incremental processing more or less.&lt;/p&gt;
&lt;h2&gt;
  
  
  How CocoIndex does incremental processing, by examples
&lt;/h2&gt;

&lt;p&gt;We could take a look at a few examples to understand what CocoIndex handles behind the scenes for incremental processing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 1: Update a document
&lt;/h3&gt;

&lt;p&gt;Consider this scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  I have a document. Initially, it's split into 5 chunks, resulting in 5 rows with their embeddings in the index. &lt;/li&gt;
&lt;li&gt;  After it's updated, 3 of them are exactly the same as previous. Others are changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we need to keep 3 rows, remove 2 previously existing rows, and add 2 new rows. These need to happen behind the scenes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Ideally, we only recompute embeddings for the 4 new rows, and reuse them for 3 unchanged chunks.
This can save computation cost especially when the embedding API charges by usage.
CocoIndex achieves this by maintaining a cache for heavy steps like embedding, and when the input for a transformation step isn't changed, the output will be reused.&lt;/li&gt;
&lt;li&gt;  Besides, we also maintain a lineage tracking in the internal storage.
It keeps track of which rows in the index are derived from the previous version of this document, to make sure stale versions are properly removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CocoIndex takes care of this.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 2: Delete a document
&lt;/h3&gt;

&lt;p&gt;Continuing with the same example. If we delete the document later, we need to delete all 7 rows derived from the document.&lt;br&gt;
Again, this needs to be based on the lineage tracking maintained by CocoIndex.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 3: Change of the transformation flow
&lt;/h3&gt;

&lt;p&gt;The transformation flow may also be changed, for example, the chunking logic is upgraded, or a parameter passed to the chunker is adjusted.&lt;br&gt;
This may result in the following scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Before the change, the document is split into 5 chunks, resulting in 5 rows with their embeddings in the index. &lt;/li&gt;
&lt;li&gt;  After the change, they become 6 chunks: 4 of the previous chunks remain unchanged, the remaining 1 is split into 2 smaller chunks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This falls into a similar situation as document update (example 1), and CocoIndex will take care of it. The approach is similar, while this involves some additional considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We can still safely reuse embeddings for 4 unchanged chunks by the caching mechanism: this needs a prerequisite that the logic and parameters for embedding are unchanged.&lt;br&gt;
If the changed part is the embedding logic itself, we will recompute the embeddings for everything.&lt;br&gt;
CocoIndex is able to see if the logic or parameters for a step changed from the cached version, by putting this additional information in the cache key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To remove stale rows in the target index, the lineage tracking works well again.&lt;br&gt;
Note that some other systems handle stale output deletions on source update/deletion by replaying the transformation logic on the previous version of input:&lt;br&gt;
this only works well when transformation is fully deterministic and never upgraded.&lt;br&gt;
CocoIndex's lineage tracking based approach doesn't have this limitation: it tracks derived outputs even when transformation logic changes or produces different results.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Example 4: Multiple inputs involved: merge / lookup / clustering
&lt;/h3&gt;

&lt;p&gt;All examples above are simple cases: each single input row (e.g. a document) is involved independently during each specific transformation.&lt;/p&gt;

&lt;p&gt;CocoIndex is a highly customizable framework, not only limited to simple chunking and embedding.&lt;br&gt;
It allows users to perform more complex advanced transformations, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Merge&lt;/strong&gt;.&lt;br&gt;
For example, you're building an index for "all AI products", and you want to combine information from multiple sources, some products exist in one source and some in multiple.&lt;br&gt;
For each product, you want to combine information from different sources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lookup&lt;/strong&gt;. &lt;br&gt;
For example, you also have a data source about company information.&lt;br&gt;
During your transformation for each product, you want to enrich it with information of the company building the product, so a lookup for the company information is needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clustering&lt;/strong&gt;.&lt;br&gt;
For example, you want to cluster different products into scenarios, and create cluster-level summaries based on information of products in the cluster.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common theme is that during transformation, multiple input rows (coming from single or multiple sources) need to be involved at the same time.&lt;br&gt;
Once a single input row is updated or deleted, CocoIndex will need to fetch other related rows from the same or other sources.&lt;br&gt;
Here which other rows are needed is based on which are involved in the transformations.&lt;br&gt;
CocoIndex keeps track of such relationships, and will fetch related rows and trigger necessary reprocessings incrementally.&lt;/p&gt;
&lt;h2&gt;
  
  
  Change data capture (CDC)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. When source supports push change
&lt;/h3&gt;

&lt;p&gt;Some source connectors support push change.&lt;br&gt;
For example, &lt;a href="https://cocoindex.io/docs/connectors/google_drive/" rel="noopener noreferrer"&gt;Google Drive&lt;/a&gt; supports drive-level changelog and sends change notifications to your public URL,&lt;br&gt;
which is applicable for team drive and personal drive (only by OAuth, service account not supported).&lt;br&gt;
When a file is created, updated, or deleted, CocoIndex could compute based on the diff.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Google Drive Change Log.&lt;/strong&gt; - Google Drive change log is only supported on the drive level, not the file/folder level.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With service accounts, you can only enable changelog for team drives, by sharing the team drive with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  2. Metadata-based, fullscan
&lt;/h3&gt;

&lt;p&gt;All source connectors in CocoIndex provide a basic list API capability, which enables a generic change detection mechanism applicable to any data source.&lt;/p&gt;

&lt;p&gt;For example, with &lt;a href="https://cocoindex.io/docs/connectors/localfs/" rel="noopener noreferrer"&gt;local file systems&lt;/a&gt;, we can traverse all directories and subdirectories recursively to get the full list of entries and their metadata (like modification time). By comparing the current state with the previous state, CocoIndex can detect changes even without source-specific change notifications.&lt;/p&gt;

&lt;p&gt;This approach works universally across all data sources, though when the number of entries is large, performing a complete traversal can be resource-intensive.&lt;/p&gt;

&lt;p&gt;This is a generic mechanism applicable to all data sources, so all data sources can use this even if there's no source-specific change capture.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Optimizations for specific sources
&lt;/h3&gt;

&lt;p&gt;When the source connector provides advanced features to list entries, for example, listing most recently changed entries,&lt;br&gt;
CocoIndex could take advantage of that to provide more efficient change detection.&lt;/p&gt;

&lt;p&gt;For example, when changelog is not available for Google Drive (see the conditions in the tip above),&lt;br&gt;
CocoIndex could monitor the change based on last modified vs last poll time, with a periodic trigger to check modified entries.&lt;br&gt;
However, this cannot capture full change, for example when an entry has been deleted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cache
&lt;/h2&gt;

&lt;p&gt;In CocoIndex, every piece of the building block in the pipeline can be cached, keyed by &lt;a href="https://cocoindex.io/docs/advanced_topics/memoization_keys/" rel="noopener noreferrer"&gt;memoization keys&lt;/a&gt;.&lt;br&gt;
Currently, whether or not cache is enabled is decided by the implementation of the function. &lt;br&gt;
For builtin functions, if it performs heavy transformation, cache is enabled.&lt;/p&gt;

&lt;p&gt;The decision of whether a cached result can be reused comes down to a single question: did anything the function depends on change? CocoIndex answers it by reducing every input (the function arguments, declared dependencies, and context values) to a deterministic &lt;em&gt;fingerprint&lt;/em&gt;. Each value is mapped to a canonical form (primitives and containers directly, dataclasses and Pydantic models field by field, and a &lt;code&gt;pickle&lt;/code&gt; fallback for anything else), and those forms are combined into one key. If that key matches a previously stored entry, the cached output is a candidate for reuse. This is the mechanism behind every reuse decision described in the examples above: when the embedding step sees the same chunk text and the same logic, its fingerprint is unchanged, and the prior embedding is returned without recomputation.&lt;/p&gt;

&lt;p&gt;The subtle part is separating &lt;em&gt;identity&lt;/em&gt; from &lt;em&gt;freshness&lt;/em&gt;. A file's path tells you &lt;em&gt;which&lt;/em&gt; input you're looking at; its modification time and content tell you whether that input has &lt;em&gt;changed&lt;/em&gt;. Folding everything into one key works, but it forces an expensive check on every run: for a file, that means reading and hashing the whole thing each time. CocoIndex lets you split the two: keep the stable identifier (path, URL, primary key) in the memoization key, and attach a &lt;em&gt;memo state&lt;/em&gt; that validates freshness only after the fingerprint matches. A state function can do a cheap check first (compare the modification time) and fall back to the expensive one (hash the content) only when the cheap check fails. Even then, it can confirm the content is identical and reuse the cached result while quietly recording the new modification time. State validation can also run asynchronously, which is what makes freshness checks like an S3 &lt;code&gt;HEAD&lt;/code&gt; request or an &lt;code&gt;If-Modified-Since&lt;/code&gt; header practical. See &lt;a href="https://cocoindex.io/docs/advanced_topics/memoization_keys/" rel="noopener noreferrer"&gt;memoization keys&lt;/a&gt; for the full model.&lt;/p&gt;

&lt;p&gt;Custom functions can take a parameter &lt;code&gt;memo&lt;/code&gt;.&lt;br&gt;
 When &lt;code&gt;True&lt;/code&gt;, CocoIndex memoizes the result of the function for reuse during reprocessing.&lt;br&gt;
 We recommend setting this to &lt;code&gt;True&lt;/code&gt; for any function that is computationally intensive.&lt;/p&gt;

&lt;p&gt;Output will be reused if both of these are unchanged: the input data and the behavior of the function.&lt;br&gt;
When you change the function's logic, bump &lt;code&gt;version&lt;/code&gt; so the previously memoized results are invalidated.&lt;/p&gt;

&lt;p&gt;For example, this enables memoization for a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cocoindex&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;coco&lt;/span&gt;

&lt;span class="nd"&gt;@coco.fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a synchronous function you want to batch, force the async interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@coco.fn.as_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batching&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks for reading and we love to &lt;a href="https://discord.com/invite/zpA9S2DR7s" rel="noopener noreferrer"&gt;hear&lt;/a&gt; from you!&lt;br&gt;
Browse the &lt;a href="https://github.com/cocoindex-io/cocoindex" rel="noopener noreferrer"&gt;CocoIndex source on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>datascience</category>
      <category>wecoded</category>
      <category>python</category>
    </item>
  </channel>
</rss>
