diff --git a/docs/web/basics/data-types.mdx b/docs/web/basics/data-types.mdx index 0b0d35faa..ee3cde9af 100644 --- a/docs/web/basics/data-types.mdx +++ b/docs/web/basics/data-types.mdx @@ -6,6 +6,35 @@ icon: shapes keywords: ["schema", "data types", "arrow", "json", "blob", "bfloat16", "vector", "geospatial"] --- +import { + PyMultimodalImports as MultimodalImports, + TsMultimodalImports as TsMultimodalImports, + RsMultimodalImports as RsMultimodalImports, + PyCreateDummyData as CreateDummyData, + TsCreateDummyData as TsCreateDummyData, + RsCreateDummyData as RsCreateDummyData, + PyDefineSchema as DefineSchema, + TsDefineSchema as TsDefineSchema, + RsDefineSchema as RsDefineSchema, + PyIngestData as IngestData, + TsIngestData as TsIngestData, + RsIngestData as RsIngestData, + PySearchData as SearchData, + TsSearchData as TsSearchData, + RsSearchData as RsSearchData, + PyProcessResults as ProcessResults, + TsProcessResults as TsProcessResults, + RsProcessResults as RsProcessResults, + PyBlobApiSchema as BlobApiSchema, + TsBlobApiSchema as TsBlobApiSchema, + RsBlobApiSchema as RsBlobApiSchema, + PyBlobApiIngest as BlobApiIngest, + TsBlobApiIngest as TsBlobApiIngest, + RsBlobApiIngest as RsBlobApiIngest, + PyBlobApiToPandas as BlobApiToPandas, + PyQueryToPandasKwargs as QueryToPandasKwargs, +} from '/snippets/multimodal.mdx'; + A table's schema is an **Arrow schema**. There is no separate LanceDB type system mapped onto storage -- a column is the same object to the query engine, to the format, and to any other engine reading the files. @@ -110,7 +139,202 @@ instead, unless the read is configured otherwise. Worth knowing before writing a predicate against one. -See [Working with multimodal data](/tables/multimodal). +### Store binary data {#store-binary-data} + +To store binary data, define a binary Arrow field in your schema (`pa.binary()` in Python, `Binary` in TypeScript, and `DataType::Binary` in Rust). + +#### 1\. Setup and imports {#1-setup-and-imports} + +First, import the necessary libraries for LanceDB and Arrow in your SDK. + + + + {MultimodalImports} + + + + {TsMultimodalImports} + + + + {RsMultimodalImports} + + + +#### 2\. Prepare data {#2-prepare-data} + +For this example, we'll create some dummy in-memory images. In a real application, you would read these from files or an API. The key is to convert your data (image, audio, etc.) into a raw `bytes` object. + + + + {CreateDummyData} + + + + {TsCreateDummyData} + + + + {RsCreateDummyData} + + + +#### 3\. Define the schema {#3-define-the-schema} + +When creating the table, it is **highly recommended** to define the schema explicitly. This ensures that your binary data is correctly interpreted as a `binary` type by Arrow/LanceDB and not as a generic string or list. + + + + {DefineSchema} + + + + {TsDefineSchema} + + + + {RsDefineSchema} + + + +#### 4\. Ingest data {#4-ingest-data} + +Now, create the table using the data and the defined schema. + + + + {IngestData} + + + + {TsIngestData} + + + + {RsIngestData} + + + +### Retrieve and use blobs {#retrieve-and-use-blobs} + +When you search your LanceDB table, you can retrieve the binary column just like any other metadata. + + + + {SearchData} + + + + {TsSearchData} + + + + {RsSearchData} + + + +#### Convert bytes back to objects {#convert-bytes-back-to-objects} + +Once you have the bytes back from the search result, you can decode them into the original format (for example, an image object or audio buffer). + + + + {ProcessResults} + + + + {TsProcessResults} + + + + {RsProcessResults} + + + +### Large Blobs (Blob API) {#large-blobs-blob-api} + +For larger files like high-resolution images or videos, Lance provides a specialized **Blob API**. By using a large-binary Arrow type (`pa.large_binary()` in Python, `LargeBinary` in TypeScript, and `DataType::LargeBinary` in Rust) and specific metadata, you enable **lazy loading** and optimized encoding. This allows you to work with massive datasets without loading all binary data into memory upfront. + +#### 1\. Define a blob schema {#1-define-a-blob-schema} + +To use the Blob API, you must mark the column with `{"lance-encoding:blob": "true"}` metadata. + + + + {BlobApiSchema} + + + + {TsBlobApiSchema} + + + + {RsBlobApiSchema} + + + +#### 2\. Ingest large blobs {#2-ingest-large-blobs} + +You can then ingest data normally, and Lance will handle the optimized storage. + + + + {BlobApiIngest} + + + + {TsBlobApiIngest} + + + + {RsBlobApiIngest} + + + + +For more advanced usage, including random access and file-like reading of blobs, see the +Lance format's [blob API documentation](https://lance.org/guide/blob/). + + +#### 3\. Convert blob tables to pandas {#3-convert-blob-tables-to-pandas} + +When you call `to_pandas()` on a local LanceDB table that contains Blob API columns, the `blob_mode` argument controls how those columns materialize. This is available in the Python SDK on local tables; remote tables raise `NotImplementedError`. + +`blob_mode` accepts: + +- `"lazy"` (default): returns blob columns as lazy `BlobFile` objects without eagerly materializing their payloads. Use this when you want to stream blob bytes on demand or only inspect a subset of rows. Namespace-backed local tables also use the Lance native blob-aware pandas conversion for lazy blobs; in-memory datasets fall back to the standard PyArrow `to_pandas()` path. +- `"bytes"`: eagerly materializes each blob as `bytes`. Use this when you need the raw payload in the DataFrame, for example to decode an image or audio clip in-process. +- `"descriptions"`: returns blob descriptors (offsets, sizes, and positions) instead of the data itself. Use this when you want to plan I/O without paying the cost of loading every blob. + +`"bytes"` and `"descriptions"` require a filesystem-backed Lance dataset and are not supported on in-memory tables. + +Extra keyword arguments are forwarded to the underlying PyArrow / Lance pandas conversion, so you can also pass options like `split_blocks` or `self_destruct`: + + + + {BlobApiToPandas} + + + +Query builders also accept `blob_mode` on their `to_pandas()` method: + +- Plain scans support `"lazy"`, `"bytes"`, and `"descriptions"` with filters, projections, aliases, `limit`, and `offset`. +- Vector, FTS, hybrid, and ordered queries can't materialize blob columns through `to_pandas()`; omit blob columns from the projection for those query shapes. +- This works on both sync and async query builders. Extra PyArrow kwargs like `split_blocks` and `self_destruct` are still forwarded. + + + + {QueryToPandasKwargs} + + + +### Other modalities {#other-modalities} + +The `pa.binary()` and `pa.large_binary()` types are universal. You can use this same pattern for other types of multimodal data: + +- **Audio:** Read `.wav` or `.mp3` files as bytes. +- **Video:** Store video transitions or full clips using the Blob API. +- **PDFs/Documents:** Store the raw file content for document search. ### bfloat16 {#bfloat16} diff --git a/docs/web/docs.json b/docs/web/docs.json index 036c022d1..53473f5a6 100644 --- a/docs/web/docs.json +++ b/docs/web/docs.json @@ -55,10 +55,6 @@ "source": "/catalog/database", "destination": "/basics/catalog" }, - { - "source": "/sql", - "destination": "/basics/apis" - }, { "source": "/api-reference", "destination": "/basics/apis" @@ -74,6 +70,14 @@ { "source": "/tables/create", "destination": "/ingestion" + }, + { + "source": "/tables/multimodal", + "destination": "/basics/data-types#blob" + }, + { + "source": "/search/select", + "destination": "/sql/select" } ], "navigation": { @@ -94,13 +98,7 @@ "basics/lance", "basics/catalog", "basics/jobs", - { - "group": "Schema & Data Types", - "pages": [ - "basics/data-types", - "tables/multimodal" - ] - }, + "basics/data-types", "tables/consistency", "basics/apis", "basics/agentic-skills" @@ -145,15 +143,11 @@ "group": "Query", "pages": [ "search/index", - "search/select", "search/vector-search", "search/multivector-search", "search/full-text-search", "search/hybrid-search", "search/filtering", - "search/explain", - "search/explain-analyze", - "sql/functions", { "group": "Reranking", "pages": [ @@ -165,6 +159,20 @@ "reranking/custom-reranker", "reranking/eval" ] + }, + "search/explain", + "search/explain-analyze", + { + "group": "SQL", + "pages": [ + "sql/index", + "sql/select", + "sql/aggregation", + "sql/join", + "sql/window", + "sql/sampling", + "sql/functions" + ] } ] }, @@ -329,6 +337,13 @@ "training/vlm-finetuning" ] }, + { + "group": "AI Agents", + "pages": [ + "build-with-ai-agents", + "agent-branch-experiments" + ] + }, { "group": "Robotics", "pages": [ @@ -356,18 +371,6 @@ ] } ] - }, - { - "tab": "Agentic Skills", - "groups": [ - { - "group": "Agentic Skills", - "pages": [ - "build-with-ai-agents", - "agent-branch-experiments" - ] - } - ] } ] }, diff --git a/docs/web/quickstart.mdx b/docs/web/quickstart.mdx index c79748e74..0bfea253a 100644 --- a/docs/web/quickstart.mdx +++ b/docs/web/quickstart.mdx @@ -454,7 +454,7 @@ These snippets load the local image file and store the bytes in an `image` colum -For more examples, see the [multimodal data](/tables/multimodal) section. +For more examples, see [Data types](/basics/data-types#blob). ## 8\. Train on the result {#8-train} diff --git a/docs/web/search/select.mdx b/docs/web/search/select.mdx deleted file mode 100644 index 35974bb18..000000000 --- a/docs/web/search/select.mdx +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: "SELECT syntax" -sidebarTitle: "SELECT" -description: "The analytical query surface: projection, filtering, grouping, joins, and how each maps onto the SDKs." -icon: table-columns -keywords: ["select", "sql", "join", "group by", "window", "cte", "datafusion"] ---- - -Enterprise SQL runs on a deployment. A local database -has no engine to plan a statement -- see -[Python, TypeScript, and SQL](/basics/apis). - -The dialect is [DataFusion's](/sql/functions), so `SELECT` behaves as its -[reference](https://datafusion.apache.org/user-guide/sql/select.html) documents. -This page is the shape of that surface and where the SDKs meet it. - -## The clauses {#clauses} - -```sql -[WITH cte AS (...)] -SELECT [ALL | DISTINCT] select_expr [, ...] -FROM from_item [[JOIN ...] ON ...] -[WHERE predicate] -[GROUP BY expr [, ...]] -[HAVING predicate] -[WINDOW name AS (...)] -[ORDER BY expr [ASC | DESC] [NULLS FIRST | LAST] [, ...]] -[LIMIT n] [OFFSET n] -``` - -Set operations -- `UNION [ALL]`, `INTERSECT`, `EXCEPT` -- combine whole queries, -and subqueries are allowed anywhere an expression or relation is. - -## Projection {#projection} - - -```sql SQL -SELECT id, title, identifier FROM documents -``` - -```python Python icon="python" -tbl.search().select(["id", "title", "identifier"]).to_arrow() -``` - -```typescript TypeScript icon="square-js" -await tbl.query().select(["id", "title", "identifier"]).toArrow(); -``` - - -Projection pushes into the scan in all three -- an unselected column is never -read off storage. On a table of images or video that is the difference between a -fast query and a slow one, so prefer naming columns over `SELECT *`. - -Row metadata is available as [pseudo-columns](/sql/functions#pseudo-columns) -(`_rowid`, `_rowaddr`, `_rowoffset`) but only when named, so they stay out of -`SELECT *`. - -## Filtering {#filtering} - - -```sql SQL -SELECT * FROM documents -WHERE identifier > 0 AND identifier < 1000000 -``` - -```python Python icon="python" -tbl.search().where("identifier > 0 AND identifier < 1000000").to_arrow() -``` - -```typescript TypeScript icon="square-js" -await tbl.query().where("identifier > 0 AND identifier < 1000000").toArrow(); -``` - - -The SDK `where` takes the same expression language, so a predicate moves between -them unchanged. A predicate on an indexed column becomes a -`ScalarIndexQuery` rather than a scan -- confirm with -[`explain_plan`](/search/explain). See [Filtering](/search/filtering) for the -operators and their pushdown behaviour. - -## Grouping and aggregation {#grouping} - -```sql -SELECT category, count(*) AS n, avg(score) AS mean_score -FROM documents -GROUP BY category -HAVING count(*) > 10 -ORDER BY n DESC -``` - -Aggregates, statistical and approximate functions come from DataFusion's -[aggregate function set](https://datafusion.apache.org/user-guide/sql/aggregate_functions.html). - - -The SDK query builders do not aggregate. Grouping, joins, and window functions -are the reason to reach for SQL rather than a query builder -- see -[choosing between them](/basics/apis#choosing). - - -## Joins {#joins} - -```sql -SELECT d.title, c.name -FROM documents AS d -JOIN categories AS c ON c.id = d.category_id -``` - -`INNER`, `LEFT`, `RIGHT`, `FULL`, and `CROSS` joins are supported. Tables in -different [namespaces](/namespaces) are joined by their dotted paths: - -```sql -SELECT * FROM prod.events.clicks AS c -JOIN prod.reference.users AS u ON u.id = c.user_id -``` - -## Window functions {#windows} - -```sql -SELECT - title, - score, - row_number() OVER (PARTITION BY category ORDER BY score DESC) AS rank -FROM documents -``` - -The full set is DataFusion's -[window functions](https://datafusion.apache.org/user-guide/sql/window_functions.html). - -## Common table expressions {#ctes} - -```sql -WITH recent AS ( - SELECT * FROM documents WHERE created_at > now() - INTERVAL '7 days' -) -SELECT category, count(*) FROM recent GROUP BY category -``` - -A CTE that you want to keep is a [view](/management/view); one whose rows you -want stored is a -[materialized view](/feature-engineering/materialized-views). - -## Search inside SELECT {#search} - -Full-text search is a **table function**, not a predicate, because it produces -relevance alongside the rows and `WHERE` has nowhere to put that: - -```sql -SELECT id, text, _score -FROM fts('my_docs', 'puppy') -ORDER BY _score DESC -LIMIT 5 -``` - -Because it is a relation it composes -- join it, aggregate over it, wrap it in a -CTE. For a keyword test that needs no score, the -[`contains_tokens`](/sql/functions#contains-tokens) predicate is -index-accelerated and usable directly in `WHERE`. - - -Vector search has no SQL form. It is a query-builder operation -- see -[Vector search](/search/vector-search). A query that needs both vector -similarity and SQL analytics runs the search in the SDK and the analytics over -its result. - - -## Reading the plan {#plans} - -`EXPLAIN` and `EXPLAIN ANALYZE` work as DataFusion defines them, and produce the -same operators the SDK methods do: - -```sql -EXPLAIN ANALYZE SELECT * FROM documents WHERE identifier < 1000 -``` - -See [Explain](/search/explain) and [Explain Analyze](/search/explain-analyze) -for how to read the output. diff --git a/docs/web/sql/aggregation.mdx b/docs/web/sql/aggregation.mdx new file mode 100644 index 000000000..71b9e8003 --- /dev/null +++ b/docs/web/sql/aggregation.mdx @@ -0,0 +1,84 @@ +--- +title: "Aggregation" +sidebarTitle: "Aggregation" +description: "GROUP BY, HAVING, and the aggregate function set." +icon: layer-group +keywords: ["group by", "having", "count", "sum", "avg", "aggregate", "approximate"] +--- + +Enterprise [SQL runs on a deployment.](/sql) + +Aggregation is one of the two main reasons to reach for SQL rather than a query +builder -- the SDKs project, filter and search, but they do not group. The other +is [joins](/sql/join). + +## Grouping {#grouping} + +```sql +SELECT category, count(*) AS n, avg(score) AS mean_score +FROM documents +GROUP BY category +HAVING count(*) > 10 +ORDER BY n DESC +``` + +`WHERE` filters rows before grouping; `HAVING` filters groups after. Putting a +row-level predicate in `HAVING` still gives the right answer but does more work, +because rows that could have been discarded early are grouped first. + +Grouping by several expressions produces one row per distinct combination: + +```sql +SELECT category, source, count(*) FROM documents GROUP BY category, source +``` + +## Aggregate without grouping {#whole-table} + +Omitting `GROUP BY` aggregates the whole table into one row -- the usual way to +ask how big something is: + +```sql +SELECT count(*), min(created_at), max(created_at) FROM documents +``` + +`count(*)` counts rows; `count(col)` counts non-null values, which is how you +measure how populated a [computed column](/feature-engineering/add-computed-column) +is before deciding to refresh it again. + +## The function set {#functions} + +The aggregates are DataFusion's, and the full list is in its +[aggregate function reference](https://datafusion.apache.org/user-guide/sql/aggregate_functions.html): + +| Kind | Examples | +| --- | --- | +| General | `count`, `sum`, `min`, `max`, `avg` | +| Statistical | `stddev`, `var`, `corr`, `covar`, `regr_*` | +| Approximate | `approx_distinct`, `approx_percentile_cont`, `approx_median` | +| Collecting | `array_agg`, `string_agg`, `first_value`, `last_value` | + +The approximate ones are worth knowing about on a large table: +`approx_distinct` answers "roughly how many distinct values" for a fraction of +what `count(DISTINCT ...)` costs, which matters when the column has high +cardinality. + +## Aggregating a sample {#sampling} + +For an estimate over a very large table, aggregate a +[sample](/sql/sampling) rather than the whole thing: + +```sql +SELECT category, count(*) * 100 AS estimated +FROM documents TABLESAMPLE (1) +GROUP BY category +``` + +Scale the result by the inverse of the sample fraction, and treat it as an +estimate -- a 1% sample of a skewed column will miss rare categories entirely. + +## What aggregation does not do {#not} + +Aggregates run over rows the query already produced. They cannot combine with +[vector search](/search/vector-search) in one statement, because similarity +search has no SQL form. Run the search in an SDK and aggregate its result, or +materialize the search output first. diff --git a/docs/web/sql/index.mdx b/docs/web/sql/index.mdx new file mode 100644 index 000000000..91bed36da --- /dev/null +++ b/docs/web/sql/index.mdx @@ -0,0 +1,63 @@ +--- +title: "SQL" +sidebarTitle: "Overview" +description: "Analytical SQL over the lakehouse: the dialect, where it runs, and what it can reach." +icon: table-list +keywords: ["sql", "datafusion", "flight sql", "analytics", "dialect"] +--- + +Enterprise SQL runs on a deployment. A local database +has no engine to plan a statement, so `execute_query` raises there -- see +[SDKs and SQL](/basics/apis). + +The dialect is [Apache DataFusion's](/sql/functions), so anything its reference +documents for that version works here unchanged. What these pages cover is the +parts that are LanceDB's: how a query reaches the engine, how Lance columns +appear to it, and the extensions a lakehouse over multimodal data needs. + +## Where a query runs {#execution} + +Queries execute on the server. The client sends a statement and receives Arrow +record batches -- nothing is converted to pandas or Python objects unless you +ask for it. A query feeding training or downstream processing therefore pays no +serialization cost to get there. + +Results stream. `execute_query` hands back a reader as soon as the first batch +is ready rather than when the query finishes, so a large result set does not +have to fit in memory before you can start reading it. See +[Query and job](/basics/jobs#queries). + +## What differs from a warehouse {#differences} + +Two things, and both come from the storage being Lance rather than something +SQL-shaped: + +- **Columns are Arrow types**, not a SQL type system mapped onto storage. A + vector column is a `FixedSizeList` and behaves like one. See + [Data types](/basics/data-types). +- **Row metadata is available as columns** -- `_rowid`, `_rowaddr`, + `_rowoffset` -- but only when named, so they stay out of `SELECT *`. + +## The statements {#statements} + +| | | +| --- | --- | +| [SELECT](/sql/select) | projection, filtering, ordering, set operations, CTEs | +| [Aggregation](/sql/aggregation) | `GROUP BY`, `HAVING`, aggregate functions | +| [Joins](/sql/join) | inner, outer, cross, and joining across namespaces | +| [Window functions](/sql/window) | `OVER (...)`, ranking, running totals | +| [Sampling](/sql/sampling) | `TABLESAMPLE`, for a random subset | +| [Functions and types](/sql/functions) | the function set, and what LanceDB adds | + +Beyond reading, SQL also writes (`INSERT`, `UPDATE`, `DELETE`, `COPY`), manages +the catalog (`CREATE TABLE`, `CREATE VIEW`, `DROP`, `SHOW`) and drives feature +engineering (`CREATE FUNCTION`, `REFRESH COLUMN`, `REFRESH MATERIALIZED VIEW`). +Those are documented with the operation rather than here -- see +[Management](/management) and [Feature engineering](/feature-engineering). + + +Vector search has no SQL form. It is a query-builder operation -- see +[Vector search](/search/vector-search). Full-text search does: the +[`fts()`](/sql/functions#fts) table function and the +[`contains_tokens`](/sql/functions#contains-tokens) predicate. + diff --git a/docs/web/sql/join.mdx b/docs/web/sql/join.mdx new file mode 100644 index 000000000..a2a2a9813 --- /dev/null +++ b/docs/web/sql/join.mdx @@ -0,0 +1,71 @@ +--- +title: "Joins" +sidebarTitle: "Join" +description: "Combining rows from more than one table, including across namespaces." +icon: link +keywords: ["join", "inner", "left", "outer", "cross", "namespace"] +--- + +Enterprise [SQL runs on a deployment.](/sql) + +Joining is the other thing the query builders cannot do. A question spanning two +tables is a SQL question. + +## The join types {#types} + +```sql +SELECT d.title, c.name +FROM documents AS d +JOIN categories AS c ON c.id = d.category_id +``` + +| | What it keeps | +| --- | --- | +| `[INNER] JOIN` | rows matching on both sides | +| `LEFT [OUTER] JOIN` | every left row, nulls where the right has no match | +| `RIGHT [OUTER] JOIN` | every right row | +| `FULL [OUTER] JOIN` | every row from both | +| `CROSS JOIN` | every combination -- the product of the two | + +`USING (col)` is shorthand for an equality join on a shared column name, and +avoids repeating it in the select list. + +## Across namespaces {#namespaces} + +Tables in different [namespaces](/namespaces) join by their dotted paths -- the +catalog is one catalog, so there is nothing to federate: + +```sql +SELECT c.id, u.email +FROM prod.events.clicks AS c +JOIN prod.reference.users AS u ON u.id = c.user_id +``` + +## Joining a search result {#search} + +[`fts()`](/sql/functions#fts) is a table function, so its result is a relation +and joins like any other: + +```sql +SELECT c.name, count(*) AS hits +FROM fts('my_docs', 'puppy') AS m +JOIN categories AS c ON c.id = m.category_id +GROUP BY c.name +ORDER BY hits DESC +``` + +That composition is the reason full-text search is a relation rather than a +predicate -- a `WHERE` clause has nowhere to put the relevance score. + +## Performance {#performance} + +A join key with a [scalar index](/indexing/scalar-index) on it lets the planner +look rows up rather than scan for them. Check with +[`explain_plan`](/search/explain): an indexed key shows a `ScalarIndexQuery`, +an unindexed one shows a `LanceScan` feeding the join. + + +Avoid selecting blob or vector columns you do not need on either side of a join. +The join multiplies rows before projection is applied in some plans, so a wide +column can be materialized many more times than the result has rows. + diff --git a/docs/web/sql/sampling.mdx b/docs/web/sql/sampling.mdx new file mode 100644 index 000000000..e45dd1980 --- /dev/null +++ b/docs/web/sql/sampling.mdx @@ -0,0 +1,101 @@ +--- +title: "Random sampling" +sidebarTitle: "Sampling" +description: "TABLESAMPLE: draw a random subset of a table, by rows or by fragments." +icon: dice +keywords: ["tablesample", "sampling", "bernoulli", "system", "repeatable", "random"] +--- + +Enterprise [SQL runs on a deployment.](/sql) + +`TABLESAMPLE` draws a random subset of a table. On a lakehouse table that is +often the difference between a question you can afford to ask and one you +cannot -- an aggregate over 1% of forty billion rows answers in a fraction of +the time, and for most estimates that is enough. + +```sql +SELECT * FROM documents TABLESAMPLE (10) -- 10 percent +SELECT * FROM documents TABLESAMPLE (500 ROWS) -- 500 rows +``` + +A bare number is a **percentage**. Add `ROWS` for a count instead. + +## Rows or fragments {#methods} + +The standard calls these two methods of one clause. They are different +operations, and the difference matters on Lance: + +```sql +SELECT * FROM documents TABLESAMPLE BERNOULLI (10) +SELECT * FROM documents TABLESAMPLE SYSTEM (10) +``` + +| Method | Aliases | What it picks | +| --- | --- | --- | +| `BERNOULLI` | `ROW` | rows, each considered independently | +| `SYSTEM` | `BLOCK` | **Lance fragments**, and takes whatever rows they hold | + +`BERNOULLI` gives a genuinely uniform sample and reads across the whole table to +get it. `SYSTEM` reads far less -- it skips entire fragments -- but the sample is +only as uniform as the data's layout. If rows were written in an order that +correlates with what you are measuring, a fragment sample inherits that +correlation. + +Reach for `SYSTEM` when you want a cheap look at the data and `BERNOULLI` when +the number you compute has to be defensible. Omitting the method gives +`BERNOULLI`. + +## Repeatability {#repeatable} + +Without a seed, every execution resamples -- running the same query twice gives +different rows. `REPEATABLE` fixes the draw: + +```sql +SELECT * FROM documents TABLESAMPLE (10) REPEATABLE (42) +``` + +This is what makes a sample usable as a **fixed evaluation set**: the same seed +against the same data gives the same rows, so a metric computed today is +comparable with one computed last week. + +## Where the clause can go {#placement} + +It attaches to a table in `FROM`, including inside a subquery, a CTE, or either +side of a join: + +```sql +SELECT * FROM (SELECT * FROM documents) d TABLESAMPLE (10) + +SELECT * FROM a TABLESAMPLE (5 ROWS) JOIN b ON a.id = b.id + +SELECT * FROM t a TABLESAMPLE (10) JOIN t b TABLESAMPLE (10) ON a.id = b.id +``` + +Each side of a join samples independently. Sampling both sides of a join +shrinks the *result* far more than either fraction suggests, because a row only +survives if both its sides were drawn. + +## What is refused {#refused} + +Sampling is semantics, not a hint. A clause that were read and then not applied +would be a wrong answer rather than a missing feature, so anything that cannot +be honoured exactly is an error: + +| | | +| --- | --- | +| `TABLESAMPLE (101)` | a percentage outside `0..=100` is refused, not clamped | +| `TABLESAMPLE (1.5 ROWS)` | a row count must be whole | +| `TABLESAMPLE (BUCKET 1 OUT OF 4)` | bucket sampling is not supported | +| the `SAMPLE` spelling | use `TABLESAMPLE` | +| the `OFFSET` modifier | not supported | + + +`TABLESAMPLE` is also refused on a WAL-backed table, where the rows a sample +would draw from are not all in the fragments yet. + + +## Sampling outside SQL {#sdk} + +For a training loop rather than a query, the SDKs read a table in a shuffled +order without going through SQL at all -- see [Training](/training), where a +permutation drives the data loader. diff --git a/docs/web/sql/select.mdx b/docs/web/sql/select.mdx new file mode 100644 index 000000000..a913cc4d8 --- /dev/null +++ b/docs/web/sql/select.mdx @@ -0,0 +1,135 @@ +--- +title: "SELECT" +sidebarTitle: "Select" +description: "Projection, filtering, ordering, and composing queries out of other queries." +icon: table-columns +keywords: ["select", "where", "order by", "limit", "cte", "union", "distinct"] +--- + +Enterprise [SQL runs on a deployment.](/sql) + +```sql +[WITH cte AS (...)] +SELECT [ALL | DISTINCT] select_expr [, ...] +FROM from_item [[JOIN ...] ON ...] +[WHERE predicate] +[GROUP BY expr [, ...]] [HAVING predicate] +[WINDOW name AS (...)] +[ORDER BY expr [ASC | DESC] [NULLS FIRST | LAST] [, ...]] +[LIMIT n] [OFFSET n] +``` + +## Projection {#projection} + + +```sql SQL +SELECT id, title, identifier FROM documents +``` + +```python Python icon="python" +tbl.search().select(["id", "title", "identifier"]).to_arrow() +``` + +```typescript TypeScript icon="square-js" +await tbl.query().select(["id", "title", "identifier"]).toArrow(); +``` + + +Projection pushes into the scan in all three -- an unselected column is never +read off storage. On a table of images or video that is the difference between a +fast query and a slow one, so **prefer naming columns over `SELECT *`**. + +Row metadata is available as [pseudo-columns](/sql/functions#pseudo-columns) +(`_rowid`, `_rowaddr`, `_rowoffset`) but only when named, so they stay out of +`SELECT *`. + +`DISTINCT` deduplicates the projected rows. + +## Filtering {#filtering} + + +```sql SQL +SELECT * FROM documents +WHERE identifier > 0 AND identifier < 1000000 +``` + +```python Python icon="python" +tbl.search().where("identifier > 0 AND identifier < 1000000").to_arrow() +``` + +```typescript TypeScript icon="square-js" +await tbl.query().where("identifier > 0 AND identifier < 1000000").toArrow(); +``` + + +The SDK `where` takes the same expression language, so a predicate moves between +them unchanged. A predicate on an indexed column becomes a `ScalarIndexQuery` +rather than a scan -- confirm with [`explain_plan`](/search/explain). See +[Filtering](/search/filtering) for the operators and their pushdown behaviour. + +## Ordering and limiting {#order-limit} + +```sql +SELECT id, title FROM documents +ORDER BY created_at DESC NULLS LAST +LIMIT 100 OFFSET 200 +``` + +`ORDER BY` on a large table is a sort over the whole result, so pair it with a +`LIMIT` unless you genuinely need every row ordered. + +## Set operations {#set-operations} + +`UNION`, `UNION ALL`, `INTERSECT` and `EXCEPT` combine whole queries: + +```sql +SELECT id FROM documents WHERE category = 'film' +UNION ALL +SELECT id FROM archived_documents WHERE category = 'film' +``` + +`UNION` deduplicates and `UNION ALL` does not, which makes `UNION ALL` the +cheaper one when you already know the inputs are disjoint. + +## Common table expressions {#ctes} + +```sql +WITH recent AS ( + SELECT * FROM documents WHERE created_at > now() - INTERVAL '7 days' +) +SELECT category, count(*) FROM recent GROUP BY category +``` + +A CTE is scoped to the statement. One you want to keep is a +[view](/management/view); one whose rows you want stored is a +[materialized view](/management/materialized-view). + +## Subqueries {#subqueries} + +Subqueries are allowed anywhere an expression or a relation is -- in `FROM`, in +`WHERE` with `IN` or `EXISTS`, and as a scalar in the select list: + +```sql +SELECT title +FROM documents +WHERE category_id IN (SELECT id FROM categories WHERE active) +``` + +## Namespaced tables {#namespaces} + +A table inside a [namespace](/namespaces) is written as a dotted path: + +```sql +SELECT * FROM prod.events.clicks +``` + +## Reading the plan {#plans} + +`EXPLAIN` and `EXPLAIN ANALYZE` work as DataFusion defines them, and produce the +same operators the SDK methods do: + +```sql +EXPLAIN ANALYZE SELECT * FROM documents WHERE identifier < 1000 +``` + +See [Explain](/search/explain) and [Explain Analyze](/search/explain-analyze). diff --git a/docs/web/sql/window.mdx b/docs/web/sql/window.mdx new file mode 100644 index 000000000..ab92405d0 --- /dev/null +++ b/docs/web/sql/window.mdx @@ -0,0 +1,90 @@ +--- +title: "Window functions" +sidebarTitle: "Window" +description: "Compute over a set of rows related to the current one, without collapsing them." +icon: chart-simple +keywords: ["window", "over", "partition by", "row_number", "rank", "lag", "lead"] +--- + +Enterprise [SQL runs on a deployment.](/sql) + +A window function computes across a set of rows related to the current one and +returns a value **per row**. That is the difference from +[aggregation](/sql/aggregation), which collapses the rows it covers. + +```sql +SELECT + title, + score, + row_number() OVER (PARTITION BY category ORDER BY score DESC) AS rank +FROM documents +``` + +Every row survives; each one gains its rank within its category. + +## The window {#window} + +```sql +OVER ( + [PARTITION BY expr [, ...]] + [ORDER BY expr [, ...]] + [ROWS | RANGE BETWEEN frame_start AND frame_end] +) +``` + +`PARTITION BY` splits the rows into independent groups. `ORDER BY` orders them +inside each partition -- required by the ranking functions, since a rank without +an order is meaningless. The frame clause narrows the window further, which is +how running totals and moving averages are expressed: + +```sql +SELECT + created_at, + sum(amount) OVER (ORDER BY created_at ROWS UNBOUNDED PRECEDING) AS running_total, + avg(amount) OVER (ORDER BY created_at ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS trailing_7 +FROM transactions +``` + +A `WINDOW` clause names a definition when several functions share one: + +```sql +SELECT + row_number() OVER w, + lag(score) OVER w +FROM documents +WINDOW w AS (PARTITION BY category ORDER BY score DESC) +``` + +## The functions {#functions} + +| Kind | Functions | +| --- | --- | +| Ranking | `row_number`, `rank`, `dense_rank`, `percent_rank`, `ntile`, `cume_dist` | +| Offset | `lag`, `lead`, `first_value`, `last_value`, `nth_value` | +| Aggregate | any [aggregate](/sql/aggregation) used with `OVER` | + +The full set is DataFusion's +[window function reference](https://datafusion.apache.org/user-guide/sql/window_functions.html). + +## Deduplicating with a window {#dedup} + +The common practical use: keep one row per key, chosen by some ordering. A +window ranks, and an outer query filters: + +```sql +WITH ranked AS ( + SELECT *, row_number() OVER (PARTITION BY doc_id ORDER BY version DESC) AS rn + FROM revisions +) +SELECT * FROM ranked WHERE rn = 1 +``` + +The outer query is needed because a window function cannot appear in `WHERE` -- +windows are computed after filtering, so the predicate would have nothing to +read. + + +Window functions sort within each partition, so on a large table they are among +the more expensive things you can ask for. Filter first, and check the cost with +[Explain Analyze](/search/explain-analyze). + diff --git a/docs/web/tables/multimodal.mdx b/docs/web/tables/multimodal.mdx deleted file mode 100644 index 6da5000b4..000000000 --- a/docs/web/tables/multimodal.mdx +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: Multimodal Data (Blobs) -sidebarTitle: "Working with multimodal data" -description: Learn how to store and query multimodal data (images, audio, video) directly in LanceDB using binary columns. -icon: "images" -keywords: ["blob", "large binary", "blobs", "multimodal"] ---- - -import { - PyMultimodalImports as MultimodalImports, - TsMultimodalImports as TsMultimodalImports, - RsMultimodalImports as RsMultimodalImports, - PyCreateDummyData as CreateDummyData, - TsCreateDummyData as TsCreateDummyData, - RsCreateDummyData as RsCreateDummyData, - PyDefineSchema as DefineSchema, - TsDefineSchema as TsDefineSchema, - RsDefineSchema as RsDefineSchema, - PyIngestData as IngestData, - TsIngestData as TsIngestData, - RsIngestData as RsIngestData, - PySearchData as SearchData, - TsSearchData as TsSearchData, - RsSearchData as RsSearchData, - PyProcessResults as ProcessResults, - TsProcessResults as TsProcessResults, - RsProcessResults as RsProcessResults, - PyBlobApiSchema as BlobApiSchema, - TsBlobApiSchema as TsBlobApiSchema, - RsBlobApiSchema as RsBlobApiSchema, - PyBlobApiIngest as BlobApiIngest, - TsBlobApiIngest as TsBlobApiIngest, - RsBlobApiIngest as RsBlobApiIngest, - PyBlobApiToPandas as BlobApiToPandas, - PyQueryToPandasKwargs as QueryToPandasKwargs, -} from '/snippets/multimodal.mdx'; - -LanceDB handles multimodal data—images, audio, video, and PDF files—natively by storing the raw bytes in a binary column alongside your vectors and metadata. This approach simplifies your data infrastructure by keeping the raw assets and their embeddings in the same database, eliminating the need for separate object storage for many use cases. - -This guide demonstrates how to ingest, store, and retrieve image data using standard binary columns, and also introduces the **Lance Blob API** for optimized handling of larger multimodal files. - -## Store binary data {#store-binary-data} - -To store binary data, define a binary Arrow field in your schema (`pa.binary()` in Python, `Binary` in TypeScript, and `DataType::Binary` in Rust). - -### 1\. Setup and imports {#1-setup-and-imports} - -First, import the necessary libraries for LanceDB and Arrow in your SDK. - - - - {MultimodalImports} - - - - {TsMultimodalImports} - - - - {RsMultimodalImports} - - - -### 2\. Prepare data {#2-prepare-data} - -For this example, we'll create some dummy in-memory images. In a real application, you would read these from files or an API. The key is to convert your data (image, audio, etc.) into a raw `bytes` object. - - - - {CreateDummyData} - - - - {TsCreateDummyData} - - - - {RsCreateDummyData} - - - -### 3\. Define the schema {#3-define-the-schema} - -When creating the table, it is **highly recommended** to define the schema explicitly. This ensures that your binary data is correctly interpreted as a `binary` type by Arrow/LanceDB and not as a generic string or list. - - - - {DefineSchema} - - - - {TsDefineSchema} - - - - {RsDefineSchema} - - - -### 4\. Ingest data {#4-ingest-data} - -Now, create the table using the data and the defined schema. - - - - {IngestData} - - - - {TsIngestData} - - - - {RsIngestData} - - - -## Retrieve and use blobs {#retrieve-and-use-blobs} - -When you search your LanceDB table, you can retrieve the binary column just like any other metadata. - - - - {SearchData} - - - - {TsSearchData} - - - - {RsSearchData} - - - -### Convert bytes back to objects {#convert-bytes-back-to-objects} - -Once you have the bytes back from the search result, you can decode them into the original format (for example, an image object or audio buffer). - - - - {ProcessResults} - - - - {TsProcessResults} - - - - {RsProcessResults} - - - -## Large Blobs (Blob API) {#large-blobs-blob-api} - -For larger files like high-resolution images or videos, Lance provides a specialized **Blob API**. By using a large-binary Arrow type (`pa.large_binary()` in Python, `LargeBinary` in TypeScript, and `DataType::LargeBinary` in Rust) and specific metadata, you enable **lazy loading** and optimized encoding. This allows you to work with massive datasets without loading all binary data into memory upfront. - -### 1\. Define a blob schema {#1-define-a-blob-schema} - -To use the Blob API, you must mark the column with `{"lance-encoding:blob": "true"}` metadata. - - - - {BlobApiSchema} - - - - {TsBlobApiSchema} - - - - {RsBlobApiSchema} - - - -### 2\. Ingest large blobs {#2-ingest-large-blobs} - -You can then ingest data normally, and Lance will handle the optimized storage. - - - - {BlobApiIngest} - - - - {TsBlobApiIngest} - - - - {RsBlobApiIngest} - - - - -For more advanced usage, including random access and file-like reading of blobs, see the -Lance format's [blob API documentation](https://lance.org/guide/blob/). - - -### 3\. Convert blob tables to pandas {#3-convert-blob-tables-to-pandas} - -When you call `to_pandas()` on a local LanceDB table that contains Blob API columns, the `blob_mode` argument controls how those columns materialize. This is available in the Python SDK on local tables; remote tables raise `NotImplementedError`. - -`blob_mode` accepts: - -- `"lazy"` (default): returns blob columns as lazy `BlobFile` objects without eagerly materializing their payloads. Use this when you want to stream blob bytes on demand or only inspect a subset of rows. Namespace-backed local tables also use the Lance native blob-aware pandas conversion for lazy blobs; in-memory datasets fall back to the standard PyArrow `to_pandas()` path. -- `"bytes"`: eagerly materializes each blob as `bytes`. Use this when you need the raw payload in the DataFrame, for example to decode an image or audio clip in-process. -- `"descriptions"`: returns blob descriptors (offsets, sizes, and positions) instead of the data itself. Use this when you want to plan I/O without paying the cost of loading every blob. - -`"bytes"` and `"descriptions"` require a filesystem-backed Lance dataset and are not supported on in-memory tables. - -Extra keyword arguments are forwarded to the underlying PyArrow / Lance pandas conversion, so you can also pass options like `split_blocks` or `self_destruct`: - - - - {BlobApiToPandas} - - - -Query builders also accept `blob_mode` on their `to_pandas()` method: - -- Plain scans support `"lazy"`, `"bytes"`, and `"descriptions"` with filters, projections, aliases, `limit`, and `offset`. -- Vector, FTS, hybrid, and ordered queries can't materialize blob columns through `to_pandas()`; omit blob columns from the projection for those query shapes. -- This works on both sync and async query builders. Extra PyArrow kwargs like `split_blocks` and `self_destruct` are still forwarded. - - - - {QueryToPandasKwargs} - - - -## Other modalities {#other-modalities} - -The `pa.binary()` and `pa.large_binary()` types are universal. You can use this same pattern for other types of multimodal data: - -- **Audio:** Read `.wav` or `.mp3` files as bytes. -- **Video:** Store video transitions or full clips using the Blob API. -- **PDFs/Documents:** Store the raw file content for document search.