Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Queries return their rows in one request when they finish within
`async_after_ms` (default 1000; `None` = always async), using the inline
Arrow response. Slower or larger queries still poll as before.
- `connect()` falls back to `HOTDATA_API_URL`, `HOTDATA_API_KEY` and
`HOTDATA_WORKSPACE` when `api_url`, `token` or `workspace_id` is not passed.
`database_id` and `async_after_ms` are also accepted in `hotdata://` URLs.
- `create_index`, `list_indexes` and `drop_index` on the backend, including
IVF vector indexes (`algorithm`, `nlist`, `probe_fraction`,
`vector_precision`) and embedding-provider indexes.
- `vector_distance` UDF, and `semantic_search` accepts a text query for
embedding-backed indexes.

### Changed

- Require `hotdata>=0.12,<0.13` (was `>=0.11,<0.12`).
- Instant-database table loads now retry while another load holds the table
lock (uses the SDK's top-level `ConnectionsApi`/`DatabasesApi`).

## [0.6.0] - 2026-10-01

### Changed
Expand Down
67 changes: 49 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,21 @@ con = ibis.hotdata.connect(
default_connection=None, # default catalog (connection id); auto-detected if only one exists
default_schema=None, # default schema; auto-detected if only one exists
database_id=None, # bind an existing instant database id at connect time
async_after_ms=1000, # queries finishing within this return rows in one request;
# slower ones go async. None = always async
poll_interval_s=0.25, # polling interval for async queries
poll_timeout_s=600.0, # max time to wait for a query result
)
```

`api_url`, `token` and `workspace_id` fall back to the `HOTDATA_API_URL`,
`HOTDATA_API_KEY` and `HOTDATA_WORKSPACE` environment variables (same as the
Hotdata CLI), so with those set this is enough:

```python
con = ibis.hotdata.connect()
```

URL-style also works, with the same parameters as query string keys:

```python
Expand Down Expand Up @@ -210,28 +220,48 @@ result = semantic_search(t, "embedding", query_vector, k=10, distance_fn=l2_dist
vector column excluded from the output — the SQL shape Hotdata's query engine requires to
route the query through its HNSW index instead of a brute-force scan.

Creating the HNSW index itself isn't wrapped by this package yet — use the `hotdata` SDK
directly:
### Text queries

For a text column indexed with an embedding provider, pass the query as a string.
Hotdata embeds it with the index's provider (`vector_distance(col, 'text')`):

```python
from hotdata import ApiClient, Configuration
from hotdata.api.indexes_api import IndexesApi
from hotdata.models.create_index_request import CreateIndexRequest

api = IndexesApi(ApiClient(Configuration(...)))
api.create_index(
connection_id=connection_id,
var_schema="main",
table="docs",
create_index_request=CreateIndexRequest(
index_name="docs_embedding_idx",
index_type="vector",
columns=["embedding"],
metric="cosine",
),
result = semantic_search(t, "body", "how do refunds work?", k=10).execute()
```

The text column stays in the output; vector (array) columns such as the generated
`body_embedding` are left out.

### Managing indexes

Create, list and drop indexes on instant-database tables (`database=` works as in
`create_table`):

```python
db = (db_id, "main")

# HNSW (default): fastest search, memory grows with the table
con.create_index("docs", "embedding", name="docs_idx", database=db, metric="cosine")

# IVF: for tables too large for HNSW; tune recall/speed with probe_fraction
con.create_index(
"docs", "embedding", name="docs_idx", database=db,
algorithm="ivf", metric="cosine", probe_fraction=0.1,
)

# Hotdata generates the embeddings from a text column
con.create_index(
"docs", "body", name="docs_body_idx", database=db,
embedding_provider_id="ep_...",
)

con.list_indexes("docs", database=db)
con.drop_index("docs", "docs_idx", database=db)
```

`index_type` can also be `"bm25"` (full-text) or `"sorted"`. The index builds within
one request, so raise `timeout=` on `connect` for large tables.

## Connecting to existing sources

If you have existing databases or warehouses connected to your Hotdata workspace (Postgres, Snowflake, BigQuery, etc.), you can query them through the same Ibis connection:
Expand Down Expand Up @@ -265,13 +295,14 @@ con.list_tables(database=(conn_id, "public")) # tables
| `create_database` / `drop_database` (managed) | ✅ |
| `create_table` from pandas / PyArrow / schema-only | ✅ |
| `drop_table` | ✅ |
| `create_index` / `list_indexes` / `drop_index` (vector HNSW/IVF, BM25, sorted) | ✅ |
| `con.table(...)` with full schema metadata | ✅ |
| Ibis expressions: filter, select, join, group\_by, agg, order\_by, limit | ✅ |
| `con.sql(...)` raw SQL | ✅ |
| `.execute()` → pandas, `.to_pyarrow()`, `.to_pyarrow_batches()` | ✅ |
| `list_catalogs`, `list_databases`, `list_tables` | ✅ |
| Arrow / Parquet column types (timestamp, decimal, list, duration, …) | ✅ |
| Vector search (`ibis_hotdata.vector.semantic_search`) | ✅ |
| Vector search (`ibis_hotdata.vector.semantic_search`, vector or text query) | ✅ |
| Temporary tables | ❌ |
| In-memory tables (`ibis.memtable(...)`) | ❌ |
| Python UDFs | ❌ |
Expand Down
15 changes: 10 additions & 5 deletions examples/06_semantic_search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Vector search: create a managed table with an embedding column, query it with
``ibis_hotdata.vector.semantic_search``.
"""Vector search: create a managed table with an embedding column, index it with
``con.create_index``, query it with ``ibis_hotdata.vector.semantic_search``.

Uses small toy 4-dimensional vectors (not real embeddings) split into two obvious
clusters -- "pets" and "finance" -- so the nearest-neighbor ordering is easy to
Expand Down Expand Up @@ -51,10 +51,15 @@ def write(api_url: str, token: str, workspace_id: str) -> str:
con = ibis.hotdata.connect(api_url=api_url, token=token, workspace_id=workspace_id)
database_id = con.create_database(DATABASE, tables=["docs"], schema=SCHEMA)
con.create_table("docs", DOCS, database=(database_id, SCHEMA), overwrite=True)
con.disconnect()

# Uploads are async; give the load a moment to finish before querying.
# Uploads are async; give the load a moment to finish before indexing.
time.sleep(2)
con.create_index(
"docs", "embedding", name="docs_embedding_idx", database=(database_id, SCHEMA),
metric="cosine",
)
print(con.list_indexes("docs", database=(database_id, SCHEMA)))
con.disconnect()
return database_id


Expand Down Expand Up @@ -90,7 +95,7 @@ def main() -> None:
token = os.environ["HOTDATA_API_KEY"]
workspace_id = os.environ["HOTDATA_WORKSPACE"]

print("== CREATE + WRITE (con.create_database / con.create_table) ==")
print("== CREATE + WRITE + INDEX (create_database / create_table / create_index) ==")
database_id = write(API_BASE_URL, token, workspace_id)
print(f"created database_id={database_id}")

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"ibis-framework>=12,<13",
"hotdata>=0.11,<0.12",
"hotdata>=0.12,<0.13",
"pyarrow>=16",
"pyarrow-hotfix>=0.6",
"pandas>=2",
Expand Down
2 changes: 2 additions & 0 deletions src/ibis_hotdata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
l2_distance,
negative_dot_product,
semantic_search,
vector_distance,
)

__all__ = [
Expand All @@ -20,4 +21,5 @@
"l2_distance",
"negative_dot_product",
"semantic_search",
"vector_distance",
]
Loading
Loading