alvinsnow

Snowflake Cortex Search · Hands-on guide

Cortex Search SQL vs Snowsight

Build the same search service two ways: with one SQL statement, or with the Create Search Service wizard in Snowsight. Then search support transcripts by meaning and filter them by region.

The test searchinternet issuesregion = North America

Overview

Two ways, one service

A Cortex Search Service indexes a text column for hybrid keyword and semantic search, and keeps that index fresh as the source changes. Define it in SQL or click through the wizard: either way you get the same kind of object, queried the same way.

Path A · SQL

One CREATE statement

Source query, search column, filters, freshness and model in one script. Join tables, chunk text or add computed columns in the query, and keep the script in Git.

Go to Path A →
Path B · Snowsight GUI

The Create Search Service wizard

Pick a table, or a stage of PDF files, choose what to search and filter, and Snowsight runs the SQL for you. Test the results in the Playground.

Go to Path B →

How a search service works

1 · Source

Your table or query

The text to search, plus any columns to filter on or return.

NeedsChange tracking, Time Travel retention above zero, and a query that supports incremental refresh.

2 · Indexing

Embed and index

Your warehouse runs the source query. Snowflake embeds the search column, builds a hybrid keyword and vector index, and refreshes it within the target lag.

Billed asWarehouse credits, only when a refresh finds changes, plus embedding tokens for new or changed rows.

3 · Serving

Answer searches

Snowflake-managed serving compute answers queries with low latency and returns the columns you ask for.

Billed asGB per month of indexed data while the service is available, even with no queries. Auto-suspend pauses it when idle.

Query it with SEARCH_PREVIEW for tests, the REST API or the Python API in applications, or as a tool for Cortex Agents.

How the steps map

StepPath A · SQLPath B · Snowsight
0Data and accessShared setup: the same table, builder role and warehouse Step 0
1Name and locationCREATE CORTEX SEARCH SERVICE in the current schema A1New service: role, warehouse, schema and name B2
2Source dataAS ( SELECT ... FROM ... ) A1Select data›Table or view B3
3Search columnON transcript_text A1Select search column B4
4Filters and resultsATTRIBUTES region and the SELECT list A1Select attributes, Select columns B4
5Freshness, model, computeTARGET_LAG, EMBEDDING_MODEL, WAREHOUSE A1Configure indexing B5
6TestSEARCH_PREVIEW, REST API, Python A3A4Playground B7
7Share and tuneSame SQL for both paths: GRANT USAGE and ALTER ... SET A5A6

Same result either way: a Cortex Search Service in CORTEX_DEMO_DB.SUPPORT_SEARCH that answers the test search. Sharing, tuning and cost control use the same SQL for both paths A5A6.

Before you start

Your names

Type your object names once. Every code block and GUI step on this page updates as you type, and Copy always copies the current version. Leave a field empty to keep the sample name.

Showing sample names

Use uppercase names. Snowsight double-quotes the name of a service built in the wizard, so an uppercase name matches the SQL on this page exactly.

Before you start

Names used in this guide

Database CORTEX_DEMO_DB · Schema SUPPORT_SEARCH · Indexing warehouse CORTEX_SEARCH_WH · Builder role CORTEX_SEARCH_ROLE · Your Snowflake user <your_user> · App users' role <app_role> · Account URL (host only) <account_url> · Path A service name TRANSCRIPT_SEARCH_SVC · Path B service name TRANSCRIPT_SEARCH_GUI

Step 0 · Both paths

Shared setup

Run this once in a Snowsight worksheet as an administrator. Both paths use the same table, role and warehouse.

0.1Create the sample table and an indexing warehouse

The four rows come from the Cortex Search documentation. The warehouse is only for search: Snowflake recommends a dedicated warehouse, MEDIUM or smaller, for each search service.

SQL
USE ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS CORTEX_DEMO_DB;
CREATE SCHEMA IF NOT EXISTS CORTEX_DEMO_DB.SUPPORT_SEARCH;

-- A dedicated warehouse for indexing (Snowflake recommends MEDIUM or smaller)
CREATE WAREHOUSE IF NOT EXISTS CORTEX_SEARCH_WH
  WAREHOUSE_SIZE = 'XSMALL'
  AUTO_SUSPEND = 60
  AUTO_RESUME = TRUE
  INITIALLY_SUSPENDED = TRUE;

USE WAREHOUSE CORTEX_SEARCH_WH;
USE SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH;

-- Sample data from the Cortex Search documentation
CREATE OR REPLACE TABLE SUPPORT_TRANSCRIPTS (
  transcript_text VARCHAR,
  region          VARCHAR,
  agent_id        VARCHAR
);

INSERT INTO SUPPORT_TRANSCRIPTS VALUES
  ('My internet has been down since yesterday, can you help?', 'North America', 'AG1001'),
  ('I was overcharged for my last bill, need an explanation.', 'Europe', 'AG1002'),
  ('How do I reset my password? The email link is not working.', 'Asia', 'AG1003'),
  ('I received a faulty router, can I get it replaced?', 'North America', 'AG1004');

-- Search services refresh with change tracking. Turn it on as the table owner.
ALTER TABLE SUPPORT_TRANSCRIPTS SET CHANGE_TRACKING = TRUE;

Why change tracking? Search services refresh incrementally, so every source table needs change tracking and a Time Travel retention above zero. Snowflake tries to turn change tracking on when you create the service; doing it here as the table owner avoids a failure if the builder role cannot alter the table.

0.2Create a role for building search services

The builder role needs the SNOWFLAKE.CORTEX_USER database role, CREATE CORTEX SEARCH SERVICE on the schema, SELECT on the source table and USAGE on the warehouse. Both paths use this role.

SQL
USE ROLE SECURITYADMIN;
CREATE ROLE IF NOT EXISTS CORTEX_SEARCH_ROLE;
GRANT ROLE CORTEX_SEARCH_ROLE TO USER <your_user>;
GRANT ROLE CORTEX_SEARCH_ROLE TO ROLE SYSADMIN;

USE ROLE SYSADMIN;
GRANT USAGE ON WAREHOUSE CORTEX_SEARCH_WH TO ROLE CORTEX_SEARCH_ROLE;
GRANT USAGE ON DATABASE CORTEX_DEMO_DB TO ROLE CORTEX_SEARCH_ROLE;
GRANT USAGE ON SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH TO ROLE CORTEX_SEARCH_ROLE;
GRANT SELECT ON TABLE CORTEX_DEMO_DB.SUPPORT_SEARCH.SUPPORT_TRANSCRIPTS TO ROLE CORTEX_SEARCH_ROLE;
GRANT CREATE CORTEX SEARCH SERVICE ON SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH TO ROLE CORTEX_SEARCH_ROLE;

USE ROLE ACCOUNTADMIN;
GRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE CORTEX_SEARCH_ROLE;

Searching is granted separately. People who only search need USAGE on the service, its database and its schema. You grant that in A5, after reading the owner's rights warning there.

0.3Choose the embedding model

The model turns the search column into vectors. Pick it before you build, because you can't change it afterwards.

English only
snowflake-arctic-embed-m-v1.5, the default when you leave the model out.
Other languages, or a mix
A multilingual model such as snowflake-arctic-embed-l-v2.0, for example for Bahasa Indonesia mixed with English. Both paths in this guide use it.

Changing the model later means CREATE OR REPLACE, which rebuilds the index. Each model has its own price per million tokens, and some models are only available in certain regions, so check availability in your region first.

Path A · SQL

Build it with SQL

One statement defines what to search, what to filter on, how fresh to stay and which model to use. Run each block in a Snowsight worksheet.

A1Create the search service

Run this as the builder role. The index is built as part of the statement, so it returns when the service can answer queries. On large tables that can take a while.

SQL
USE ROLE CORTEX_SEARCH_ROLE;
USE WAREHOUSE CORTEX_SEARCH_WH;
USE SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH;

CREATE OR REPLACE CORTEX SEARCH SERVICE TRANSCRIPT_SEARCH_SVC
  ON transcript_text
  ATTRIBUTES region
  WAREHOUSE = CORTEX_SEARCH_WH
  TARGET_LAG = '1 day'
  EMBEDDING_MODEL = 'snowflake-arctic-embed-l-v2.0'
  COMMENT = 'Support transcripts: search by meaning, filter by region'
  AS (
    SELECT transcript_text, region, agent_id
    FROM CORTEX_DEMO_DB.SUPPORT_SEARCH.SUPPORT_TRANSCRIPTS
  );
ON transcript_text
The column people search. It must be text. Snowflake embeds it for semantic search and uses the full text for keyword search.
ATTRIBUTES region
Columns you can filter on at query time. They must also be in the source query.
WAREHOUSE
Runs the source query, builds the index and keeps it fresh.
TARGET_LAG
How far the index may fall behind the source table. Refreshes process changes incrementally.
EMBEDDING_MODEL
Optional. Leave it out for the English-only default. Fixed after creation.
AS ( SELECT ... )
The source query. Every column it returns, here including agent_id, can come back in results.

Long text? Split it into chunks first. Snowflake recommends chunks of no more than 512 tokens, about 385 English words, and SPLIT_TEXT_RECURSIVE_CHARACTER can do the splitting. See the PDF example D2.

Have a unique ID? If a text column such as a ticket ID identifies each row, add PRIMARY KEY (ticket_id) after the ON clause. Services with a primary key can use an optimized refresh path that cuts the cost and latency of refreshes.

Replacing a live service? With CREATE OR REPLACE, the old index stops serving when the statement commits, and the new one serves once it is built.

A2Check that it is ready

SQL
USE ROLE CORTEX_SEARCH_ROLE;
SHOW CORTEX SEARCH SERVICES IN SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH;
DESCRIBE CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC;

Look at INDEXING_STATE and SERVING_STATE. If indexing stops, INDEXING_ERROR shows the SQL error from the source query. Indexing suspends itself after five refresh failures in a row; fix the cause, then run ALTER CORTEX SEARCH SERVICE ... RESUME INDEXING.

A3Test it with SEARCH_PREVIEW

Run the test search from a worksheet: transcripts about internet issues, only in North America.

SQL
SELECT PARSE_JSON(
  SNOWFLAKE.CORTEX.SEARCH_PREVIEW(
    'CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC',
    '{
      "query": "internet issues",
      "columns": ["transcript_text", "region", "agent_id"],
      "filter": {"@eq": {"region": "North America"}},
      "limit": 2
    }'
  )
)['results'] AS results;

To see the results as rows, flatten them. Try words that don't appear in any transcript, such as billing problem: semantic search can still match the transcript about an overcharged bill.

SQL
SELECT
  value['transcript_text']::TEXT AS transcript_text,
  value['region']::TEXT          AS region,
  value['agent_id']::TEXT        AS agent_id
FROM TABLE(FLATTEN(PARSE_JSON(
  SNOWFLAKE.CORTEX.SEARCH_PREVIEW(
    'CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC',
    '{"query": "billing problem", "columns": ["transcript_text", "region", "agent_id"], "limit": 3}'
  )
)['results']));

For testing only. SEARCH_PREVIEW checks a service from SQL. It takes string literals only, returns at most 300 KB and is slower than the REST and Python APIs, so applications should use A4.

Filters work on ATTRIBUTES columns with @eq, @contains, @gte, @lte and @primarykey, combined with @and, @or and @not.

A4Query it from an application

Applications call the REST endpoint or the Python API with the same request: the query text, the columns to return, an optional filter and a limit.

Shell
# PAT holds a programmatic access token for a user whose role can use the service
curl -X POST "https://<account_url>/api/v2/databases/CORTEX_DEMO_DB/schemas/SUPPORT_SEARCH/cortex-search-services/TRANSCRIPT_SEARCH_SVC:query" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $PAT" \
  -d '{
    "query": "internet issues",
    "columns": ["transcript_text", "region", "agent_id"],
    "filter": {"@eq": {"region": "North America"}},
    "limit": 5
  }'
Python
from snowflake.core import Root
from snowflake.snowpark import Session

# Fill in your connection parameters: account, user, authentication and role
session = Session.builder.configs({...}).create()
root = Root(session)

svc = (root.databases["CORTEX_DEMO_DB"]
           .schemas["SUPPORT_SEARCH"]
           .cortex_search_services["TRANSCRIPT_SEARCH_SVC"])

resp = svc.search(
    query="internet issues",
    columns=["transcript_text", "region", "agent_id"],
    filter={"@eq": {"region": "North America"}},
    limit=5,
)
print(resp.to_json())

The REST API accepts programmatic access tokens, key-pair JWTs and OAuth. The Python example needs version 0.8.0 or later of the snowflake package (pip install snowflake -U). Responses can be up to 10 MB.

Plan for HTTP 429. Cortex Search returns 429 when requests arrive too fast, so retry with backoff. For more than 20 queries per second on one service, contact your Snowflake account team.

A5Share it with the people who search

Read this before you grant

Searches run with the owner's rights. Any role with USAGE on the service can retrieve every row and column the service indexed, even rows that role cannot read in the source table, for example because of row-level policies. Index only data that every user of the service may see, or build one service per audience.

SQL
USE ROLE SYSADMIN;
GRANT USAGE ON DATABASE CORTEX_DEMO_DB TO ROLE <app_role>;
GRANT USAGE ON SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH TO ROLE <app_role>;

USE ROLE CORTEX_SEARCH_ROLE;
GRANT USAGE ON CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC TO ROLE <app_role>;

-- Path B service (skip if you only built Path A)
GRANT USAGE ON CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_GUI TO ROLE <app_role>;

Searching needs USAGE on the service, its database and its schema. No grant on SUPPORT_TRANSCRIPTS is needed, which is exactly why the warning above matters.

A6Control cost and freshness

Each service has the three cost meters shown in How it works. These commands tune them.

SQL
USE ROLE CORTEX_SEARCH_ROLE;

-- Pause serving after 30 idle minutes (Preview, minimum 1800 seconds)
ALTER CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC SET AUTO_SUSPEND = 1800;

-- Set how fresh the index must be
ALTER CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC SET TARGET_LAG = '1 hour';

-- Process new rows now instead of waiting for the next refresh
ALTER CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC REFRESH;

-- Pause or resume by hand (needs OPERATE on the service)
ALTER CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC SUSPEND SERVING;
ALTER CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC RESUME SERVING;

Auto-suspend is in Preview. After the idle period it pauses serving, and the next query resumes it. Resuming can take a few minutes, and requests in that window get HTTP 429 with a Retry-After header.

See what it costs. Run these as a role that can read SNOWFLAKE.ACCOUNT_USAGE.

SQL
-- Serving, embedding and batch credits per service per day
SELECT usage_date, service_name, consumption_type, model_name, tokens, credits
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_SEARCH_DAILY_USAGE_HISTORY
WHERE database_name = 'CORTEX_DEMO_DB'
ORDER BY usage_date DESC, service_name;

-- Refresh compute, on the dedicated indexing warehouse
SELECT start_time, end_time, credits_used
FROM SNOWFLAKE.ACCOUNT_USAGE.WAREHOUSE_METERING_HISTORY
WHERE warehouse_name = 'CORTEX_SEARCH_WH'
ORDER BY start_time DESC;

The usage view covers serving, embedding tokens and batch search, but not warehouse compute, which is one more reason to give each service its own warehouse. To cap spend, set up a resource budget for Cortex Search.

Path B · Snowsight GUI

Build it in Snowsight

The Create Search Service wizard asks the same questions as the CREATE statement, one screen at a time.

About the screenshots. They come from a Snowflake demo account, so role, warehouse, schema and service names differ from this guide, and a personal schema name is hidden. Some screens were captured while indexing PDFs from a stage; the table flow uses the same screens for the shared steps. Labels can change between Snowsight releases. Click a screenshot to enlarge it.

B1Open Cortex Search

  1. Open your user menu at the bottom left of Snowsight, select Switch role and choose CORTEX_SEARCH_ROLE.
  2. In the left navigation, select AI & ML›Cortex Search.
  3. Click Create at the top right.
Snowsight Cortex Search page with Playground and Create buttons at the top right
Cortex Search page. Each service is listed with its indexing and serving state, rows and target lag. Create and Playground sit at the top right.

B2Name the service

  • Role and warehouse: CORTEX_SEARCH_ROLE and CORTEX_SEARCH_WH. The warehouse runs the source query when the service is created and each time it refreshes.
  • Service database and schema: CORTEX_DEMO_DB.SUPPORT_SEARCH
  • Service name: TRANSCRIPT_SEARCH_GUI
  • Click Next.

Use uppercase. Snowsight double-quotes the service name. An uppercase name works in SQL with or without quotes, while a lowercase one such as demo_test must always be written as "demo_test".

New service screen with role, warehouse, database, schema and service name fields
New service. Role and warehouse, the database and schema, and the service name. The panel on the left lists the wizard steps.

B3Select the data

  • Select Table or view. Stage (Preview) is the PDF route, covered in D1.
  • Choose CORTEX_DEMO_DB › SUPPORT_SEARCH › SUPPORT_TRANSCRIPTS and check the Data preview.
  • Click Next.

The wizard indexes one table or view. For joins, transformations or several sources, create a view first or use SQL.

Select data to be indexed, with Table or view and Stage options
Select data to be indexed. Table or view for text in columns, Stage for files.

B4Choose what to search, filter and return

Three short screens follow. Click Next after each one. Their order can differ between Snowsight releases, so follow the step list on the left.

Wizard screenWhat to pickSame as in SQL
Select search columntranscript_textON transcript_text
Select attributesregion, or Skip this option if you need no filtersATTRIBUTES region
Select columnstranscript_text, region, agent_idThe SELECT list in AS ( ... )

B5Configure indexing

  • Target Lag: 1 day, as in Path A.
  • Embedding model: snowflake-arctic-embed-l-v2.0, as in Path A. The default, snowflake-arctic-embed-m-v1.5, is English only, and you can't change the model later.
  • Warehouse for indexing: CORTEX_SEARCH_WH
  • Optional: click Show advanced options, turn Auto-suspend (Preview) on and enter at least 30 minutes.
  • Click Create.
Configure your Search Service screen with target lag, embedding model, warehouse and auto-suspend
Configure your Search Service. Target lag, embedding model, indexing warehouse and, under advanced options, auto-suspend. This capture comes from the PDF flow, so it also shows a parsing notice.

B6Watch it build

  1. After you click Create, a message says Creating search service and points you to Query History. Open Query History to follow the statements the wizard runs.
  2. In the captured demo, those statements include CREATE CORTEX SEARCH SERVICE. Copy it into a script if you want to keep the service as code A1.
  3. Back on the Cortex Search page, find TRANSCRIPT_SEARCH_GUI and check its indexing and serving state.
Message reading Creating search service, track progress in Query History
Creating search service. The message points to Query History, where you can follow the SQL the wizard runs.

B7Test it in the Playground

  1. On the Cortex Search page, choose CORTEX_DEMO_DB.SUPPORT_SEARCH in the database and schema filter.
  2. Click Playground at the top right.
  3. Make sure TRANSCRIPT_SEARCH_GUI is the selected service, type internet issues and review the results.

The Playground does not support multi-index services. To test the exact region filter from the top of this page, run the query in B8.

B8Use the same SQL from here on

A service built in the wizard is a normal Cortex Search Service. Check, query, share and tune it with A2 to A6, using TRANSCRIPT_SEARCH_GUI as the name. For example:

SQL
USE ROLE CORTEX_SEARCH_ROLE;
DESCRIBE CORTEX SEARCH SERVICE CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_GUI;

SELECT PARSE_JSON(
  SNOWFLAKE.CORTEX.SEARCH_PREVIEW(
    'CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_GUI',
    '{"query": "internet issues", "columns": ["transcript_text", "region"], "filter": {"@eq": {"region": "North America"}}, "limit": 2}'
  )
)['results'] AS results;

If the service name has lowercase letters, wrap it in double quotes inside the string as well.

Variation · Documents

Search PDFs in a stage

The wizard can parse and chunk PDF files for you, as a Preview feature. In SQL you run the same three stages yourself: parse, chunk, then index the chunks.

D1In Snowsight (Preview)

  1. In B3, select Stage, choose the stage and the PDF files, then click Next.
  2. Parsing strategy: Text embedding for documents that are mostly text, or Multimodal embedding for documents with diagrams, images or drawings.
  3. Automated processing: choose the Destination schema for the table of parsed content that the wizard creates. Under Show advanced options you can set a prefix for new objects and pick a chunking strategy:
    • Chunk by size: 1,800 characters with a 300-character overlap by default. Snowflake recommends 1,800 characters, about 512 tokens, or fewer.
    • No chunking: one chunk per document, for short documents.
    • Split by page: one chunk per page, when each page has little text.
  4. Configure indexing as in B5 and click Create. Parsing can take several minutes, and the service has no data until it finishes.
Parsing strategy screen with Multimodal embedding and Text embedding options
Parsing strategy. Multimodal embedding for visual documents, Text embedding for mostly text.
Automated processing screen with destination schema, object prefix and chunking strategy
Automated processing. The wizard creates a table for parsed content. Advanced options set an object prefix and the chunking strategy.

D2With SQL

This follows Snowflake's PDF tutorial with this guide's names: AI_PARSE_DOCUMENT extracts the text and layout, and SPLIT_TEXT_MARKDOWN_HEADER splits it on headers.

SQL
-- The document route also needs CREATE STAGE and CREATE TABLE
USE ROLE SYSADMIN;
GRANT CREATE STAGE, CREATE TABLE ON SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH TO ROLE CORTEX_SEARCH_ROLE;

USE ROLE CORTEX_SEARCH_ROLE;
USE WAREHOUSE CORTEX_SEARCH_WH;
USE SCHEMA CORTEX_DEMO_DB.SUPPORT_SEARCH;

-- A stage for the PDF files, with a directory table
CREATE OR REPLACE STAGE DOCS_STAGE
  DIRECTORY = (ENABLE = TRUE)
  ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE');

Upload the PDFs in Snowsight: Catalog›Explorer › CORTEX_DEMO_DB › SUPPORT_SEARCH › Stages › DOCS_STAGE, click + Files at the top right, drop the files and click Upload. Then parse, chunk and index:

SQL
-- Refresh the directory table so it lists the uploaded files
ALTER STAGE DOCS_STAGE REFRESH;

-- 1. Parse: extract text and layout from each PDF
CREATE OR REPLACE TABLE DOCS_RAW_TEXT AS
SELECT
  relative_path,
  TO_VARCHAR(
    AI_PARSE_DOCUMENT(
      TO_FILE('@CORTEX_DEMO_DB.SUPPORT_SEARCH.DOCS_STAGE', relative_path),
      {'mode': 'LAYOUT'}
    ):content
  ) AS extracted_layout
FROM DIRECTORY('@CORTEX_DEMO_DB.SUPPORT_SEARCH.DOCS_STAGE')
WHERE relative_path LIKE '%.pdf';

-- 2. Chunk: split on the top two Markdown headers, 2,000 characters, 300 overlap
CREATE OR REPLACE TABLE DOCS_CHUNKS AS
SELECT
  relative_path,
  BUILD_SCOPED_FILE_URL(@CORTEX_DEMO_DB.SUPPORT_SEARCH.DOCS_STAGE, relative_path) AS file_url,
  (
    relative_path || ':\n'
    || COALESCE('Header 1: ' || c.value['headers']['header_1'] || '\n', '')
    || COALESCE('Header 2: ' || c.value['headers']['header_2'] || '\n', '')
    || c.value['chunk']
  ) AS chunk,
  'English' AS language
FROM DOCS_RAW_TEXT,
  LATERAL FLATTEN(SNOWFLAKE.CORTEX.SPLIT_TEXT_MARKDOWN_HEADER(
    extracted_layout,
    OBJECT_CONSTRUCT('#', 'header_1', '##', 'header_2'),
    2000,
    300
  )) c;

-- 3. Index: search the chunks, filter by language
CREATE OR REPLACE CORTEX SEARCH SERVICE DOCS_SEARCH_SVC
  ON chunk
  ATTRIBUTES language
  WAREHOUSE = CORTEX_SEARCH_WH
  TARGET_LAG = '1 hour'
  AS (
    SELECT chunk, relative_path, file_url, language
    FROM DOCS_CHUNKS
  );

This is a one-time load. For new files, insert their chunks into DOCS_CHUNKS instead of recreating the table, because a recreated source table needs change tracking turned on again. AI_PARSE_DOCUMENT is billed separately from the search service.

Compare

Which path should you use?

Both create the same object. The difference is how much control you need and how you want to manage the service over time.

QuestionPath A · SQLPath B · Snowsight
Best forRepeatable builds, CI/CD and services over joined or transformed dataA first service, demos and quick tests with business users
Source dataAny source query that supports incremental refresh, including joins and chunkingOne table or view, or PDF files in a stage (Preview)
OptionsEvery CREATE option, such as PRIMARY KEY, REFRESH_MODE and INITIALIZETarget lag, embedding model, warehouse and auto-suspend
Service nameUnquoted unless you quote itAlways double-quoted, so case matters
TestingSEARCH_PREVIEW, the REST API and PythonThe Playground, except for multi-index services
Keeping it as codeThe script is the source of truthCopy the statement from Query History

A common pattern: agree on the search column and filters in the wizard with the business team, copy the statement from Query History B6, then keep it as a script A1 and manage access with SQL A5.

Finish

Clean up

Serving is billed while a service is available, even with no queries, so remove demo services when you are done.

SQL
-- Remove only the search services (this stops their serving cost)
USE ROLE CORTEX_SEARCH_ROLE;
DROP CORTEX SEARCH SERVICE IF EXISTS CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_SVC;
DROP CORTEX SEARCH SERVICE IF EXISTS CORTEX_DEMO_DB.SUPPORT_SEARCH.TRANSCRIPT_SEARCH_GUI;
DROP CORTEX SEARCH SERVICE IF EXISTS CORTEX_DEMO_DB.SUPPORT_SEARCH.DOCS_SEARCH_SVC;

-- Or remove everything this guide created
USE ROLE SYSADMIN;
DROP DATABASE IF EXISTS CORTEX_DEMO_DB;
DROP WAREHOUSE IF EXISTS CORTEX_SEARCH_WH;

USE ROLE SECURITYADMIN;
DROP ROLE IF EXISTS CORTEX_SEARCH_ROLE;

Reference

Further reading

Snowflake Cortex Search hands-on guide. Screenshots come from a Snowflake demo account, and Snowsight labels can change between releases. Credit rates are listed in the Snowflake Service Consumption Table.

Keep going

alvinsnow.comAll guidesPersonal site. Views are my own, not Snowflake’s.