慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
使用 Python 和 Snowflake Cortex 建立即時可用語意搜尋
Artem · 2026-05-24 · via DEV Community

最近我獲得了一項任務,要為我們的目錄實現AI驅動的語義搜尋,而我們已經在使用Snowflake,因此決定使用Cortex Search Service來實現這個功能.

如果你還不知道Cortex Search是什麼,你可以快速查看這個連結以了解概覽基本上,Cortex Search 允許您直接在 Snowflake 中已儲存的数据上建立低延遲的語義和全文搜尋

在這篇文章中,我將分享我整合 Cortex Search Service 到我們的 Python 後端應用的經驗,包括我們遇到的陷阱


保持搜尋欄位專注

TheSEARCH_TEXT 欄位,或更精確地說是 Cortex Search 的「搜尋欄位」,是 Cortex Search 索引並用於搜尋回傳的欄位.

我最初犯的一個錯誤是試圖將幾乎所有可用的欄位放入 SEARCH_TEXT 欄位.

初始時的邏輯看起來合理:Cortex Search 看到的欄位越多,它擁有的上下文就越多。但在實際操作中,這可能會讓可搜尋的文本變得雜亂。

例如,像 IDs、狀態旗標、租戶/公司 IDs、貨幣 IDs 和內部分類 IDs 等欄位通常對於語義搜尋沒有意義:

'id:' || COALESCE(id::STRING, ''),
'warehouse_id:' || COALESCE(warehouse_id::STRING, ''),
'project_id:' || COALESCE(project_id::STRING, ''),
'is_active:' || COALESCE(is_active::STRING, ''),
'status:' || COALESCE(status, ''),
'currency_id:' || COALESCE(currency_id::STRING, '')

進入全螢幕模式 離開全螢幕模式

這些欄位通常不是使用者搜尋的內容。它們應該被顯示為ATTRIBUTES並用於過濾。我會在下一節中講解。

這個SEARCH_TEXT該欄位應專注於描述項目實際意義的字段:

CONCAT_WS(
    ' ',
    title,
    brand,
    origin,
    category_name,
    subcategory_name,
    short_description
) AS SEARCH_TEXT

進入全螢幕模式 離開全螢幕模式

我們需要包含對終端用戶搜尋查詢有用的欄位。


暴露可過濾欄位為ATTRIBUTES

Cortex 搜索服務配置中的另一個小但重要的細節是ATTRIBUTES 屬性.

The ON 欄位是可搜尋的文本欄位。這是 Cortex Search 用來匹配使用者查詢的方式。但如果你也需要根據元資料篩選結果,例如組織、狀態、分類、品牌、區域或可用性,這些欄位必須在服務建立時加入 ATTRIBUTES。

屬性並非主要的搜尋文字。它們是與搜尋結果一起回傳的欄位,可供篩選或顯示。Snowflake 文件中,Cortex Search 篩選功能會作用於在 CREATE CORTEX SEARCH SERVICE 命令中指定的 ATTRIBUTES 欄位上。

CREATE OR REPLACE CORTEX SEARCH SERVICE DEMO_DB.SEARCH.PRODUCT_SEARCH_SERVICE ON SEARCH_TEXT 
ATTRIBUTES (
  CITY,
  COUNTRY,
  CURRENCY,
  IS_ACTIVE
)
WAREHOUSE = WH_SEARCH_DEMO
TARGET_LAG = '1 hour'
AS
SELECT
    ID,
    CITY,
    COUNTRY,
    CURRENCY,
    IS_ACTIVE
FROM DEMO_DB.SEARCH.PRODUCT_SEARCH_SOURCE;

進入全螢幕模式 離開全螢幕模式

要記住的重點:你後來想要過濾的每一個欄位都必須作為屬性可用。此外,Snowflake指出ATTRIBUTES中的欄位必須包含在用於建立服務的來源查詢中。

接下來我們可以在python程式中應用這些過濾器:

from typing import Any, Dict, List

COUNTRY_ATTRIBUTE = "COUNTRY"
CITY_ATTRIBUTE = "CITY"
IS_ACTIVE_ATTRIBUTE = "IS_ACTIVE"

filters: List[Dict[str, Any]] = [
    {
        "@or": [
            {
                "@eq": {
                    COUNTRY_ATTRIBUTE: "UK",
                }
            },
            {
                "@eq": {
                    CITY_ATTRIBUTE: "London",
                }
            },
        ],
    },
    {
        "@eq": {
            IS_ACTIVE_ATTRIBUTE: True,
        }
    },
]

response = search_service.search(
    query=query,
    columns=[
        "ID",
        "COUNTRY",
        "CITY",
        "IS_ACTIVE",
    ],
    filter={
        "@and": filters,
    },
    limit=20,
)

Enter fullscreen mode Exit fullscreen mode

嘗試讓過濾器選擇性強且容易理解。好的過濾器應該在Cortex Search排序和返回結果之前減少搜索空間。如果過濾器有效載荷變得太大或太複雜,這可能是一個信號,表示搜索來源表格應該調整,而不是將太多應用程式邏輯移入搜索查詢中.


注意TARGET_LAG

還有一個重要的爭論在建立或替換CORTEX搜尋服務指令TARGET_LAG.

簡單來說,TARGET_LAG控制您的CORTEX搜尋索引與來源表的更新程度

例如:

TARGET_LAG = '10 minutes'

這不代表每一個新資料列會立即變得可搜尋。Cortex Search 還需要先更新其內部索引。所以當你在來源表中插入或更新資料列時,這些變更只會在下次更新發生後才會出現在搜尋結果中。

如果你使用受管理的嵌入,這點尤其重要。Cortex Search 需要處理更新後的來源數據,創建或更新嵌入,並刷新搜索索引,才能使用戶透過語義搜索找到這些記錄.

所以如果 TARGET_LAG 太長,你的搜索結果可能會變得過時。一個新發布的項目可能已經存在於你的來源表中,但用戶可能需要一段時間才能在搜索中找到它。

同時,將TARGET_LAG設定得太低也未必是最佳方案。更頻繁的更新可能意味著背景中需要更多 Snowflake 工作,這可能會增加信用使用量。Snowflake 也提到,如果目標延遲設定得太低,索引可能會比需要時更頻繁地刷新。

因此,正確的值取決於您的搜尋結果實際需要多新。

CREATE OR REPLACE CORTEX SEARCH SERVICE DEMO_DB.SEARCH.PRODUCT_SEARCH_SERVICE
ON SEARCH_TEXT
ATTRIBUTES
(
    ITEM_ID,
    STATUS,
    IS_ACTIVE,
    ACCOUNT_ID
)
WAREHOUSE = WH_SEARCH_DEMO
TARGET_LAG = '30 minutes'
AS
SELECT
    ITEM_ID,
    SEARCH_TEXT,
    STATUS,
    IS_ACTIVE,
    ACCOUNT_ID
FROM DEMO_DB.SEARCH.PRODUCT_SEARCH_SOURCE;

Enter fullscreen mode 退出全螢幕模式

若為面向用戶的目錄搜尋,新發表的物品應快速出現,那麼15-20分鐘或許是合理的。

若為內部知識庫、文件搜尋或任何不常變更的數據,1小時甚至更長時間也完全沒問題。


選擇適合您應用場景的嵌入模型

Cortex Search 在向量搜尋階段使用嵌入模型。簡單來說,這個模型將您的搜尋欄位和用戶查詢轉換為向量,讓 Cortex Search 可以找到語義上相似的記錄,而不只是包含相同關鍵字的記錄。Snowflake 允許在建立 Cortex Search 服務時,透過 EMBEDDING_MODEL 參數選擇模型。

CREATE OR REPLACE CORTEX SEARCH SERVICE DEMO_DB.SEARCH.PRODUCT_SEARCH_SERVICE
ON SEARCH_TEXT
ATTRIBUTES
(
    ITEM_ID,
    STATUS,
    IS_ACTIVE,
    ACCOUNT_ID
)
WAREHOUSE = WH_SEARCH_DEMO
TARGET_LAG = '30 minutes'
EMBEDDING_MODEL = 'snowflake-arctic-embed-m-v1.5' # custom EMBEDDING_MODEL
AS
SELECT
    ITEM_ID,
    SEARCH_TEXT,
    STATUS,
    IS_ACTIVE,
    ACCOUNT_ID
FROM DEMO_DB.SEARCH.PRODUCT_SEARCH_SOURCE;

Enter fullscreen mode 退出全螢幕模式

Snowflake 列表snowflake-arctic-embed-m-v1.5為預設的 Cortex 搜尋嵌入模型。它有 768 個輸出維度,一個 512 個 token 的上下文視窗,並支援英文語言。Snowflake 也將其描述為在可用的 Cortex 搜尋模型中索引速度最快的選項。這使它成為英文專用目錄或索引速度至關重要的內部搜尋的優佳起點。

若您的目錄是多語言的,僅支援英文的預設模型可能不適用於您。此時,請檢查支援多語言的模型,例如snowflake-arctic-embed-l-v2.0snowflake-arctic-embed-l-v2.0-8kvoyage-multilingual-2。如需查詢支援的模型、維度、上下文視窗及語言支援的完整列表,請參考官方的Snowflake embedding models table

Snowflake的CREATE CORTEX SEARCH SERVICE 文件顯示 EMBEDDING_MODEL 為服務定義的一部分,更改它通常意味著重新創建服務,而不只是調整一個運行時參數。因此,值得早期測試這一點,特別是如果您的產品有多語言數據或用戶使用不同語言搜尋.


Cortex 連接預熱

我們整合Cortex Search的主要原因是為了減少搜尋查詢延遲,因為我們有大量的物品。

第一個瓶頸來自連接處理。我使用了Snowflake Python library,而每個新的搜尋請求都需要打開一個新的連接。僅僅是連接設置就花費了約1–1.5秒,而實際的搜尋查詢只花了500–600毫秒。所以在優化搜尋本身之前,我專注於從請求路徑中移除連接設置的成本。

此次優化是將 Snowflake 連接設定移出請求路徑。我新增了一個 warmup() 方法,它會解析一次 Cortex Search 服務,並在工作者進程層級緩存 Snowflake 連接、Root 物件和服務參考。這個緩存受鎖定保護,因此即使同時有多個請求到達,初始化也能保持安全。

    def warmup(self) -> None:
        self._get_service()

    def _get_service(self):
        cache_key = (self.database, self.schema, self.service_name)
        service = self.__class__._service_cache.get(cache_key)
        if service:
            return service

        with self.__class__._lock:
            service = self.__class__._service_cache.get(cache_key)
            if service:
                return service

            root = self._get_root()
            service = (
                root.databases[self.database]
                .schemas[self.schema]
                .cortex_search_services[self.service_name]
            )
            self.__class__._service_cache[cache_key] = service
            return service

    def _get_root(self):
        root = self.__class__._root
        connection = self.__class__._connection
        if root and connection and not connection.is_closed():
            return root

        with self.__class__._lock:
            root = self.__class__._root
            connection = self.__class__._connection
            if root and connection and not connection.is_closed():
                return root

            if connection and not connection.is_closed():
                try:
                    connection.close()
                except Exception:
                    pass

            self.__class__._connection = None
            self.__class__._root = None
            self.__class__._service_cache = {}

            try:
                connection = snowflake.connector.connect(**self._connection_parameters)
            except Exception as exc:
                raise CortexSearchCatalogServiceError(
                    f"Failed to create Snowflake connection for Cortex search: {exc}"
                )

            self.__class__._connection = connection
            root = Root(connection)
            self.__class__._root = root
            return root

Enter fullscreen mode Exit fullscreen mode

接著我從 Gunicorn 的 post_fork 鈎子呼叫了這個暖身方法。因為 Gunicorn 工作程式是獨立的進程,每個工作進程在分叉後都必須建立自己的 Snowflake 連接。透過這個變更,連接是在工作進程啟動時建立,而不是在第一次搜尋請求時建立,這消除了用戶端路徑上的 1–1.5 秒連接設定成本。

def post_fork(server, worker):
    # Warm the Snowflake Cortex client in each worker process so the first user
    # search does not pay the connection/session setup cost on the request path.

    try:
        if not _cortex_warmup_is_configured():
            server.log.debug(
                "Skipping Cortex warmup for worker pid=%s because Snowflake settings are incomplete.",
                worker.pid,
            )
            return

        from apps.db.cortex_search_services.cortex_search_catalog_service import CortexSearchCatalogService

        CortexSearchCatalogService().warmup()
        server.log.info("Cortex warmup completed for worker pid=%s", worker.pid)
    except Exception:
        server.log.exception("Cortex warmup failed for worker pid=%s", worker.pid)

Enter fullscreen mode Exit fullscreen mode


調整Cortex搜尋分數權重

接下來重要的優化不是關於延遲,而是關於結果品質。

Cortex Search 使用混合排序。它可以結合語義/向量相似度、關鍵字/文字匹配,以及神經重新排序。Snowflake 透過 scoring_config.weights 來暴露這個功能,其中向量、文字和重新排序器控制每個評分組成部分的相對貢獻。預設情況下,這些組成部分具有相等的權重,但你可以根據你的使用情況,每個查詢進行調整。

例如,在我們的情況下,純關鍵字匹配有時會因為它包含匹配的詞語而將錯誤的項目排名更高。一個真實的例子是「手套保護」的標誌被排名在實際手套之上。文本匹配很強,但在語義上它並不是用戶預期的產品.

為了解決這個問題,我增加了相對於文本分數的向量分數權重:

# Tune these weights to adjust Cortex ranking before results reach application code.
# vectors = semantic similarity, texts = keyword match, reranker = neural reranker.
# Raise vectors relative to texts to prevent keyword-heavy but semantically irrelevant
# items from outranking semantically correct ones.
DEFAULT_SCORING_CONFIG = {
    "weights": {
        "vectors": 2,
        "texts": 1,
        "reranker": 1,
    }
}

Enter fullscreen mode 退出全螢幕模式

這是一個重要的參數,因為它直接改變了Cortex Search在結果傳遞到您的應用程式代碼之前如何排序結果。如果您的搜尋更接近傳統的全文搜尋,您可能想要增加text的weight。如果您的用戶透過意義、同義詞、描述或自然語言進行搜尋,增加vector的weight可以產生更好的結果。

另一個值得測試的參數是重新排序器。Cortex Search 預設使用語義重新排序來提高相關性,但重新排序也可能增加查詢延遲。Snowflake 允許在延遲比額外的品質改進更重要的情況下禁用重新排序。


從 Cortex Search 僅僅返回您需要的数据

在您將 Cortex Search 接入生產代碼之前,有一個實際的限制值得了解,那就是有效載荷大小.

Snowflake 文檔對 Cortex Search 查詢的響應大小進行了限制:REST API 和 Python API 的響應有效載荷不得超過 10 MB

這意味著您需要謹慎處理查詢的雙方:您發送到 Cortex Search 的內容以及您要求它返回的內容。

對於過濾器,盡量只傳送真正需要的內容。過於複雜的嵌套是生產環境中靜默錯誤的最短途徑。如果你在相同字段上有大型的OR條件,在可能的情况下,優先選擇更緊湊的操作符。例如,而不是建立一個長的or列表:

filter={
    "@or": [
        {"@eq": {"STATUS": "published"}},
        {"@eq": {"STATUS": "scheduled"}},
        {"@eq": {"STATUS": "archived"}},
    ]
}

進入全螢幕模式 退出全螢幕模式

盡量使用usein 如果符合您的情况:

filter={
    "@in": {
        "STATUS": ["published", "scheduled", "archived"]
    }
}

進入全螢幕模式 退出全螢幕模式

對你的數據模型也是如此。如果每個搜尋請求都需要大量的過濾邏輯,這可能是一個信號,表示用於搜尋的模型設計得還不夠好。有時候,準備一個更乾淨的搜尋來源表格或視圖,其中包含更容易進行過濾的字段,而不是將過多的應用程式邏輯推入 Cortex Search 請求,這是值得考慮的。

回應方面,我比較喜歡保持Cortex Search結果欄位為小型。在大多數應用程式搜尋流程中,Cortex Search不需要回傳完整的物件有效負載。它只需要回傳內部ID,以及一些用於排序或除錯的輕量級欄位即可。

response = search_service.search(
    query=query,
    columns=[
        "ID",
    ],
    filter=filter,
    limit=50,
)

Enter fullscreen mode Exit fullscreen mode

然後應用程式可以取得這些 ID,並從主要應用程式資料庫中擷取完整記錄:

item_ids = [row["ID"] for row in response.results]

items = (
    CatalogItem.objects
    .filter(id__in=item_ids)
    .select_related("brand", "category")
    .prefetch_related("tags")
)

進入全螢幕模式 離開全螢幕模式

這讓 Cortex Search 聚焦於它最擅長的事:尋找相關記錄。您的應用程式資料庫仍然負責載入所有領域物件,並且您可以節省查詢成本。

結論

Cortex Search 是一個強大的選項,當您已經在 Snowflake 中有數據,並且需要低延遲的語義和全文搜索,而不需要從頭開始建立一個獨立的搜索流程時。

但重要的是,它並不是完全「設定後就忘記」。質量和性能很大程度上取決於您如何配置該服務以及您如何從後端應用程式中使用它。

我經驗中最主要的教訓是:

  • 在第一個用戶請求之前預熱 Snowflake 連接;
  • 將搜尋欄位專注於用戶實際搜尋的字段;
  • 根據您的數據和語言需求選擇嵌入模型;
  • 根據您的應用場景是否需要更多語義或關鍵字匹配來調整評分權重;
  • 如果您需要過濾,請將必要的欄位添加到 ATTRIBUTES。
  • 根據搜尋結果需要的新鮮度來設定 TARGET_LAG;
  • 僅從 Cortex Search 回傳您需要的數據,並從您的主要應用程式資料庫載入完整物件;

總體來說,Cortex Search 對應用程式層級的搜尋非常有效,特別是目錄搜尋、文件搜尋,以及其他已儲存在 Snowflake 中的數據。