title: 'Vector-Based Search' sidebar_label: 'Vector Search' description: 'Learn how Spice can perform searches using vector-based methods.' sidebar_position: 1 tags:
🎓 Learn how it works with the Amazon S3 Vectors with Spice (opens in a new tab) engineering blog post.
Vector search uses embeddings (numerical representations of text or data) to find semantically similar content. Unlike keyword search, vector search understands meaning and context, making it useful for:
For embedding columns that contain many vectors per row (for example, one vector per tag or per section), see Multi-Vector Search (opens in a new tab).
Spice supports two types of embedding providers:
Embedding models are defined in the spicepod.yaml file as top-level components.
To enable vector search, specify embeddings for the dataset columns in spicepod.yaml:
This configuration instructs Spice to create embeddings from the body column, enabling similarity searches on body content.
Execute similarity searches using Spice's HTTP API:
For detailed API documentation, see Search API Reference (opens in a new tab).
If the dataset uses chunking, Spice returns relevant chunks. To retrieve entire documents, include the embedding column in additional_columns:
Response:
The embedding index can also be used to perform search in SQL, via a user-defined table function (UDTF).
SQL Function Signature of vector_search:
By default, vector_search retrieves up to 1000 results. To adjust this limit, specify the limit parameter in the function call. When using a specific vector engine, such as s3_vectors the limit defaults to that of the vector engine.
WHERE predicates on base table columns are pushed down as pre-filters — only matching rows are scored and ranked. See Search in SQL (opens in a new tab) for details.
:::warning[Limitations]
vector_search UDTF does not yet support chunked embedding columns. Chunking support is on the roadmap.:::
A vector is indexable only if it has a defined direction under the metrics the index offers. Two shapes do not, and a row carrying one is skipped instead of stored:
| Shape | Examples | Why |
|---|---|---|
No direction — every component is 0 or NaN | [0, 0, 0], [NaN, NaN], [0, NaN] | The vector points nowhere, so a cosine distance to it is undefined. |
Non-finite — at least one component is NaN or ±Inf | [1.0, NaN], [0.0, Inf] | Every metric the index offers answers NaN or Inf for it, whatever it is compared against. |
A vector that is both is reported as the first.
Skipping is neither silent nor passive:
s3_vectors write paths warn once per record, naming it and its reason — its embedding has no direction — every component is zero or NaN, or its embedding has a NaN or infinite component, so every distance to it is undefined. The elasticsearch path aggregates instead: one warning per reason per write, carrying the count of records skipped for it and a sample of their row indices.NULL or empty.NULL is skipped too, with its own warning, but evicts nothing — there is no key with which to address an earlier entry.This criterion is applied by the elasticsearch (opens in a new tab) and s3_vectors (opens in a new tab) engines, and by the in-memory warm index written through to alongside them. The duckdb (opens in a new tab) engine's write path applies no such criterion and stores the vector as it arrives; it screens the query vector instead, failing a search whose query embedding has a non-finite component with DuckDB vector query contains a non-finite value.
A chunked (opens in a new tab) embedding column is indexed one entry per chunk, addressed by the row's primary key plus a chunk id. A write upserts the entries the row's current text produces — chunk ids 0 through n — and nothing above them. So when a row's text is rewritten into fewer chunks than it held before, the entries the longer text produced above the new count are not overwritten by that write: unless the index removes them, a search can still match a row on content its current text no longer has.
Which engine holds the entries decides whether they are removed:
| Index | Entries a shortened row superseded |
|---|---|
| The in-memory warm index, written through to alongside a vector engine | Removed by the same write. |
elasticsearch (opens in a new tab) | Removed by the same write when every primary-key column is a string, boolean, or integer column. Spice deletes the rest of each rewritten row's chunks with a _delete_by_query on the key. A key with another column type, such as a float, date, or timestamp, has no exact term form, so its superseded entries are kept. |
s3_vectors (opens in a new tab) | Kept. The store can only be addressed by a complete key, and the chunk ids a shorter text no longer produces are not known to it. |
duckdb (opens in a new tab) | Not applicable — the engine refuses a chunked column at load. |
The warm index is added alongside the engine for an accelerated dataset whose engine distance metric it can also compute — cosine, Euclidean/L2, or dot product — and Spice logs when it is left out. Where an engine keeps superseded entries and the warm index is present, a chunked column is written through both, so the warm index stops returning the superseded chunks while the engine's own store still holds them — and where the warm index is not present, nothing removes them at all. Spice reports an index that keeps them once, on its first write to it — whether or not a row has been shortened yet — naming the index and the column:
The `s3_vector_index` search index cannot remove the entries a row's previous 'content' text produced when that text is rewritten to something shorter, so such a row can stay searchable by content it no longer has and a search can return it. The index can only be addressed by a complete key, and the entries a shorter text no longer produces are not known to it. Re-create the search index to rebuild it from the rows the dataset holds now. See: https://spiceai.org/docs/features/search
The index name is s3_vector_index, or elasticsearch_index for an Elasticsearch index whose key it cannot address, and 'content' is the search column of the reader's own dataset.
A full refresh does not clear them. A refresh_mode: full (opens in a new tab) refresh re-upserts every row under the chunk ids its current text produces, but neither elasticsearch nor s3_vectors empties its store at the start of such a refresh, so in an index that keeps superseded entries a chunk id no row produces any more is never written over. The same is true of an append, an upsert, and a changes (opens in a new tab) CDC batch, each of which sees only the rows it carries. Re-creating the index is what clears them, because it is rebuilt from the rows the dataset holds now.
A row whose search text becomes NULL or empty is a separate case, handled by eviction — see Vectors an Index Will Not Store (opens in a new tab).
Spice supports vector searches on datasets with pre-existing embeddings. Ensure the dataset meets these requirements:
<original_column_name>_embedding.FixedSizeList[Float32|Float64, N]List[FixedSizeList[Float32|Float64, N]]<column_name>_offset) is required:
List[FixedSizeList[Int32, 2]], indicating chunk boundaries.Example dataset structure (sales table):
Non-chunked:
Chunked:
Underlying Column Presence:
string Arrow data type (opens in a new tab) .Embeddings Column Naming Convention:
<column_name>_embedding. For example, a customer_reviews table with a review column must have a review_embedding column.Embeddings Column Data Type:
FixedSizeList[Float32 or Float64, N], where N is the dimension (size) of the embedding vector. FixedSizeList is used for efficient storage and processing of fixed-size vectors.List[FixedSizeList[Float32 or Float64, N]].Offset Column for Chunked Data:
By following these guidelines, you can ensure that your dataset with pre-existing embeddings is fully compatible with the vector search and other embedding functionalities provided by Spice.
A table sales with an address column and corresponding embedding column(s).
<column_name>_offsetList[FixedSizeList[Int32, 2]], where each element is a pair of integers [start, end] representing the start and end indices of the chunk in the underlying text column. This offset column maps each chunk in the embeddings back to the corresponding segment in the underlying text column.[[0, 100], [101, 200]] indicates two chunks covering indices 0–100 and 101–200, respectively.