mirror of
https://github.com/lancedb/lancedb.git
synced 2025-12-23 21:39:57 +00:00
Compare commits
68 Commits
python-v0.
...
python-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
600bfd7237 | ||
|
|
d087e7891d | ||
|
|
098e397cf0 | ||
|
|
63ee8fa6a1 | ||
|
|
693091db29 | ||
|
|
dca4533dbe | ||
|
|
f6bbe199dc | ||
|
|
366e522c2b | ||
|
|
244b6919cc | ||
|
|
aca785ff98 | ||
|
|
bbdebf2c38 | ||
|
|
1336cce0dc | ||
|
|
6c83b6a513 | ||
|
|
6bec4bec51 | ||
|
|
23d30dfc78 | ||
|
|
94c8c50f96 | ||
|
|
72765d8e1a | ||
|
|
a2a8f9615e | ||
|
|
b085d9aaa1 | ||
|
|
6eb662de9b | ||
|
|
2bb2bb581a | ||
|
|
38321fa226 | ||
|
|
22749c3fa2 | ||
|
|
123a49df77 | ||
|
|
a57aa4b142 | ||
|
|
d8e3e54226 | ||
|
|
ccfdf4853a | ||
|
|
87e5d86e90 | ||
|
|
1cf8a3e4e0 | ||
|
|
5372843281 | ||
|
|
54677b8f0b | ||
|
|
ebcf9bf6ae | ||
|
|
797514bcbf | ||
|
|
1c872ce501 | ||
|
|
479f471c14 | ||
|
|
ae0d2f2599 | ||
|
|
1e8678f11a | ||
|
|
662968559d | ||
|
|
9d895801f2 | ||
|
|
80613a40fd | ||
|
|
d43ef7f11e | ||
|
|
554e068917 | ||
|
|
567734dd6e | ||
|
|
1589499f89 | ||
|
|
682e95fa83 | ||
|
|
1ad5e7f2f0 | ||
|
|
ddb3ef4ce5 | ||
|
|
ef20b2a138 | ||
|
|
2e0f251bfd | ||
|
|
2cb91e818d | ||
|
|
2835c76336 | ||
|
|
8068a2bbc3 | ||
|
|
24111d543a | ||
|
|
7eec2b8f9a | ||
|
|
b2b70ea399 | ||
|
|
e50a3c1783 | ||
|
|
b517134309 | ||
|
|
6fb539b5bf | ||
|
|
f37fe120fd | ||
|
|
2e115acb9a | ||
|
|
27a638362d | ||
|
|
22a6695d7a | ||
|
|
57eff82ee7 | ||
|
|
7732f7d41c | ||
|
|
5ca98c326f | ||
|
|
b55db397eb | ||
|
|
c04d72ac8a | ||
|
|
28b02fb72a |
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.3.3
|
current_version = 0.3.9
|
||||||
commit = True
|
commit = True
|
||||||
message = Bump version: {current_version} → {new_version}
|
message = Bump version: {current_version} → {new_version}
|
||||||
tag = True
|
tag = True
|
||||||
|
|||||||
4
.github/workflows/node.yml
vendored
4
.github/workflows/node.yml
vendored
@@ -11,6 +11,10 @@ on:
|
|||||||
- .github/workflows/node.yml
|
- .github/workflows/node.yml
|
||||||
- docker-compose.yml
|
- docker-compose.yml
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Disable full debug symbol generation to speed up CI build and keep memory down
|
# Disable full debug symbol generation to speed up CI build and keep memory down
|
||||||
# "1" means line tables only, which is useful for panic tracebacks.
|
# "1" means line tables only, which is useful for panic tracebacks.
|
||||||
|
|||||||
35
.github/workflows/npm-publish.yml
vendored
35
.github/workflows/npm-publish.yml
vendored
@@ -37,14 +37,10 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
node/vectordb-*.tgz
|
node/vectordb-*.tgz
|
||||||
|
|
||||||
node-macos:
|
node-macos-x86:
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
# Only runs on tags that matches the make-release action
|
# Only runs on tags that matches the make-release action
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
target: [x86_64-apple-darwin, aarch64-apple-darwin]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -54,11 +50,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd node
|
cd node
|
||||||
npm ci
|
npm ci
|
||||||
- name: Install rustup target
|
|
||||||
if: ${{ matrix.target == 'aarch64-apple-darwin' }}
|
|
||||||
run: rustup target add aarch64-apple-darwin
|
|
||||||
- name: Build MacOS native node modules
|
- name: Build MacOS native node modules
|
||||||
run: bash ci/build_macos_artifacts.sh ${{ matrix.target }}
|
run: bash ci/build_macos_artifacts.sh x86_64-apple-darwin
|
||||||
|
- name: Upload Darwin Artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: native-darwin
|
||||||
|
path: |
|
||||||
|
node/dist/lancedb-vectordb-darwin*.tgz
|
||||||
|
|
||||||
|
node-macos-arm64:
|
||||||
|
runs-on: macos-13-xlarge
|
||||||
|
# Only runs on tags that matches the make-release action
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: brew install protobuf
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: |
|
||||||
|
cd node
|
||||||
|
npm ci
|
||||||
|
- name: Build MacOS native node modules
|
||||||
|
run: bash ci/build_macos_artifacts.sh aarch64-apple-darwin
|
||||||
- name: Upload Darwin Artifacts
|
- name: Upload Darwin Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
20
.github/workflows/python.yml
vendored
20
.github/workflows/python.yml
vendored
@@ -8,6 +8,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- python/**
|
- python/**
|
||||||
- .github/workflows/python.yml
|
- .github/workflows/python.yml
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux:
|
linux:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
@@ -32,18 +37,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pip install -e .[tests]
|
pip install -e .[tests]
|
||||||
pip install tantivy@git+https://github.com/quickwit-oss/tantivy-py#164adc87e1a033117001cf70e38c82a53014d985
|
pip install tantivy@git+https://github.com/quickwit-oss/tantivy-py#164adc87e1a033117001cf70e38c82a53014d985
|
||||||
pip install pytest pytest-mock black isort
|
pip install pytest pytest-mock ruff
|
||||||
- name: Black
|
- name: Lint
|
||||||
run: black --check --diff --no-color --quiet .
|
run: ruff format --check .
|
||||||
- name: isort
|
|
||||||
run: isort --check --diff --quiet .
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest -m "not slow" -x -v --durations=30 tests
|
run: pytest -m "not slow" -x -v --durations=30 tests
|
||||||
- name: doctest
|
- name: doctest
|
||||||
run: pytest --doctest-modules lancedb
|
run: pytest --doctest-modules lancedb
|
||||||
mac:
|
mac:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: "macos-12"
|
strategy:
|
||||||
|
matrix:
|
||||||
|
mac-runner: [ "macos-13", "macos-13-xlarge" ]
|
||||||
|
runs-on: "${{ matrix.mac-runner }}"
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -62,8 +68,6 @@ jobs:
|
|||||||
pip install -e .[tests]
|
pip install -e .[tests]
|
||||||
pip install tantivy@git+https://github.com/quickwit-oss/tantivy-py#164adc87e1a033117001cf70e38c82a53014d985
|
pip install tantivy@git+https://github.com/quickwit-oss/tantivy-py#164adc87e1a033117001cf70e38c82a53014d985
|
||||||
pip install pytest pytest-mock black
|
pip install pytest pytest-mock black
|
||||||
- name: Black
|
|
||||||
run: black --check --diff --no-color --quiet .
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest -m "not slow" -x -v --durations=30 tests
|
run: pytest -m "not slow" -x -v --durations=30 tests
|
||||||
pydantic1x:
|
pydantic1x:
|
||||||
|
|||||||
9
.github/workflows/rust.yml
vendored
9
.github/workflows/rust.yml
vendored
@@ -10,6 +10,10 @@ on:
|
|||||||
- rust/**
|
- rust/**
|
||||||
- .github/workflows/rust.yml
|
- .github/workflows/rust.yml
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# This env var is used by Swatinem/rust-cache@v2 for the cache
|
# This env var is used by Swatinem/rust-cache@v2 for the cache
|
||||||
# key, so we set it to make sure it is always consistent.
|
# key, so we set it to make sure it is always consistent.
|
||||||
@@ -44,8 +48,11 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --all-features
|
run: cargo test --all-features
|
||||||
macos:
|
macos:
|
||||||
runs-on: macos-12
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
mac-runner: [ "macos-13", "macos-13-xlarge" ]
|
||||||
|
runs-on: "${{ matrix.mac-runner }}"
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ exclude = ["python"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
lance = { "version" = "=0.8.7", "features" = ["dynamodb"] }
|
lance = { "version" = "=0.8.20", "features" = ["dynamodb"] }
|
||||||
lance-linalg = { "version" = "=0.8.7" }
|
lance-index = { "version" = "=0.8.20" }
|
||||||
lance-testing = { "version" = "=0.8.7" }
|
lance-linalg = { "version" = "=0.8.20" }
|
||||||
|
lance-testing = { "version" = "=0.8.20" }
|
||||||
# Note that this one does not include pyarrow
|
# Note that this one does not include pyarrow
|
||||||
arrow = { version = "47.0.0", optional = false }
|
arrow = { version = "47.0.0", optional = false }
|
||||||
arrow-array = "47.0"
|
arrow-array = "47.0"
|
||||||
@@ -19,7 +20,7 @@ arrow-arith = "47.0"
|
|||||||
arrow-cast = "47.0"
|
arrow-cast = "47.0"
|
||||||
chrono = "0.4.23"
|
chrono = "0.4.23"
|
||||||
half = { "version" = "=2.3.1", default-features = false, features = [
|
half = { "version" = "=2.3.1", default-features = false, features = [
|
||||||
"num-traits"
|
"num-traits",
|
||||||
] }
|
] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
object_store = "0.7.1"
|
object_store = "0.7.1"
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
|
|
||||||
**Developer-friendly, serverless vector database for AI applications**
|
**Developer-friendly, serverless vector database for AI applications**
|
||||||
|
|
||||||
<a href="https://lancedb.github.io/lancedb/">Documentation</a> •
|
<a href='https://github.com/lancedb/vectordb-recipes/tree/main' target="_blank"><img alt='LanceDB' src='https://img.shields.io/badge/VectorDB_Recipes-100000?style=for-the-badge&logo=LanceDB&logoColor=white&labelColor=645cfb&color=645cfb'/></a>
|
||||||
<a href="https://blog.lancedb.com/">Blog</a> •
|
<a href='https://lancedb.github.io/lancedb/' target="_blank"><img alt='lancdb' src='https://img.shields.io/badge/DOCS-100000?style=for-the-badge&logo=lancdb&logoColor=white&labelColor=645cfb&color=645cfb'/></a>
|
||||||
<a href="https://discord.gg/zMM32dvNtd">Discord</a> •
|
[](https://blog.lancedb.com/)
|
||||||
<a href="https://twitter.com/lancedb">Twitter</a>
|
[](https://discord.gg/zMM32dvNtd)
|
||||||
|
[](https://twitter.com/lancedb)
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Builds the macOS artifacts (node binaries).
|
# Builds the macOS artifacts (node binaries).
|
||||||
# Usage: ./ci/build_macos_artifacts.sh [target]
|
# Usage: ./ci/build_macos_artifacts.sh [target]
|
||||||
# Targets supported: x86_64-apple-darwin aarch64-apple-darwin
|
# Targets supported: x86_64-apple-darwin aarch64-apple-darwin
|
||||||
|
set -e
|
||||||
|
|
||||||
prebuild_rust() {
|
prebuild_rust() {
|
||||||
# Building here for the sake of easier debugging.
|
# Building here for the sake of easier debugging.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
site_name: LanceDB Docs
|
site_name: LanceDB Docs
|
||||||
|
site_url: https://lancedb.github.io/lancedb/
|
||||||
repo_url: https://github.com/lancedb/lancedb
|
repo_url: https://github.com/lancedb/lancedb
|
||||||
edit_uri: https://github.com/lancedb/lancedb/tree/main/docs/src
|
edit_uri: https://github.com/lancedb/lancedb/tree/main/docs/src
|
||||||
repo_name: lancedb/lancedb
|
repo_name: lancedb/lancedb
|
||||||
@@ -97,6 +98,7 @@ nav:
|
|||||||
- YouTube Transcript Search: notebooks/youtube_transcript_search.ipynb
|
- YouTube Transcript Search: notebooks/youtube_transcript_search.ipynb
|
||||||
- Documentation QA Bot using LangChain: notebooks/code_qa_bot.ipynb
|
- Documentation QA Bot using LangChain: notebooks/code_qa_bot.ipynb
|
||||||
- Multimodal search using CLIP: notebooks/multimodal_search.ipynb
|
- Multimodal search using CLIP: notebooks/multimodal_search.ipynb
|
||||||
|
- Example - Calculate CLIP Embeddings with Roboflow Inference: examples/image_embeddings_roboflow.md
|
||||||
- Serverless QA Bot with S3 and Lambda: examples/serverless_lancedb_with_s3_and_lambda.md
|
- Serverless QA Bot with S3 and Lambda: examples/serverless_lancedb_with_s3_and_lambda.md
|
||||||
- Serverless QA Bot with Modal: examples/serverless_qa_bot_with_modal_and_langchain.md
|
- Serverless QA Bot with Modal: examples/serverless_qa_bot_with_modal_and_langchain.md
|
||||||
- 🌐 Javascript examples:
|
- 🌐 Javascript examples:
|
||||||
@@ -144,14 +146,13 @@ nav:
|
|||||||
- Serverless Chatbot from any website: examples/serverless_website_chatbot.md
|
- Serverless Chatbot from any website: examples/serverless_website_chatbot.md
|
||||||
- TransformersJS Embedding Search: examples/transformerjs_embedding_search_nodejs.md
|
- TransformersJS Embedding Search: examples/transformerjs_embedding_search_nodejs.md
|
||||||
- API references:
|
- API references:
|
||||||
- Python API: python/python.md
|
- OSS Python API: python/python.md
|
||||||
|
- SaaS Python API: python/saas-python.md
|
||||||
- Javascript API: javascript/modules.md
|
- Javascript API: javascript/modules.md
|
||||||
- LanceDB Cloud↗: https://noteforms.com/forms/lancedb-mailing-list-cloud-kty1o5?notionforms=1&utm_source=notionforms
|
- LanceDB Cloud↗: https://noteforms.com/forms/lancedb-mailing-list-cloud-kty1o5?notionforms=1&utm_source=notionforms
|
||||||
|
|
||||||
extra_css:
|
extra_css:
|
||||||
- styles/global.css
|
- styles/global.css
|
||||||
extra_javascript:
|
|
||||||
- scripts/posthog.js
|
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
analytics:
|
analytics:
|
||||||
|
|||||||
@@ -71,9 +71,41 @@ a single PQ code.
|
|||||||
### Use GPU to build vector index
|
### Use GPU to build vector index
|
||||||
|
|
||||||
Lance Python SDK has experimental GPU support for creating IVF index.
|
Lance Python SDK has experimental GPU support for creating IVF index.
|
||||||
|
Using GPU for index creation requires [PyTorch>2.0](https://pytorch.org/) being installed.
|
||||||
|
|
||||||
You can specify the GPU device to train IVF partitions via
|
You can specify the GPU device to train IVF partitions via
|
||||||
|
|
||||||
- **accelerator**: Specify to `"cuda"`` to enable GPU training.
|
- **accelerator**: Specify to ``cuda`` or ``mps`` (on Apple Silicon) to enable GPU training.
|
||||||
|
|
||||||
|
=== "Linux"
|
||||||
|
|
||||||
|
<!-- skip-test -->
|
||||||
|
``` { .python .copy }
|
||||||
|
# Create index using CUDA on Nvidia GPUs.
|
||||||
|
tbl.create_index(
|
||||||
|
num_partitions=256,
|
||||||
|
num_sub_vectors=96,
|
||||||
|
accelerator="cuda"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Macos"
|
||||||
|
|
||||||
|
<!-- skip-test -->
|
||||||
|
```python
|
||||||
|
# Create index using MPS on Apple Silicon.
|
||||||
|
tbl.create_index(
|
||||||
|
num_partitions=256,
|
||||||
|
num_sub_vectors=96,
|
||||||
|
accelerator="mps"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Trouble shootings:
|
||||||
|
|
||||||
|
If you see ``AssertionError: Torch not compiled with CUDA enabled``, you need to [install
|
||||||
|
PyTorch with CUDA support](https://pytorch.org/get-started/locally/).
|
||||||
|
|
||||||
|
|
||||||
## Querying an ANN Index
|
## Querying an ANN Index
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
There are various Embedding functions available out of the box with lancedb. We're working on supporting other popular embedding APIs.
|
There are various Embedding functions available out of the box with lancedb. We're working on supporting other popular embedding APIs.
|
||||||
|
|
||||||
## Text Embedding Functions
|
## Text Embedding Functions
|
||||||
Here are the text embedding functions registered by default
|
Here are the text embedding functions registered by default.
|
||||||
|
Embedding functions have inbuilt rate limit handler wrapper for source and query embedding function calls that retry with exponential standoff.
|
||||||
|
Each `EmbeddingFunction` implementation automatically takes `max_retries` as an argument which has the deafult value of 7.
|
||||||
|
|
||||||
### Sentence Transformers
|
### Sentence Transformers
|
||||||
Here are the parameters that you can set when registering a `sentence-transformers` object, and their default values:
|
Here are the parameters that you can set when registering a `sentence-transformers` object, and their default values:
|
||||||
@@ -66,6 +68,56 @@ actual = table.search(query).limit(1).to_pydantic(Words)[0]
|
|||||||
print(actual.text)
|
print(actual.text)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Instructor Embeddings
|
||||||
|
Instructor is an instruction-finetuned text embedding model that can generate text embeddings tailored to any task (e.g., classification, retrieval, clustering, text evaluation, etc.) and domains (e.g., science, finance, etc.) by simply providing the task instruction, without any finetuning
|
||||||
|
|
||||||
|
If you want to calculate customized embeddings for specific sentences, you may follow the unified template to write instructions:
|
||||||
|
|
||||||
|
Represent the `domain` `text_type` for `task_objective`:
|
||||||
|
|
||||||
|
* `domain` is optional, and it specifies the domain of the text, e.g., science, finance, medicine, etc.
|
||||||
|
* `text_type` is required, and it specifies the encoding unit, e.g., sentence, document, paragraph, etc.
|
||||||
|
* `task_objective` is optional, and it specifies the objective of embedding, e.g., retrieve a document, classify the sentence, etc.
|
||||||
|
|
||||||
|
More information about the model can be found here - https://github.com/xlang-ai/instructor-embedding
|
||||||
|
|
||||||
|
| Argument | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `name` | `str` | "hkunlp/instructor-base" | The name of the model to use |
|
||||||
|
| `batch_size` | `int` | `32` | The batch size to use when generating embeddings |
|
||||||
|
| `device` | `str` | `"cpu"` | The device to use when generating embeddings |
|
||||||
|
| `show_progress_bar` | `bool` | `True` | Whether to show a progress bar when generating embeddings |
|
||||||
|
| `normalize_embeddings` | `bool` | `True` | Whether to normalize the embeddings |
|
||||||
|
| `quantize` | `bool` | `False` | Whether to quantize the model |
|
||||||
|
| `source_instruction` | `str` | `"represent the docuement for retreival"` | The instruction for the source column |
|
||||||
|
| `query_instruction` | `str` | `"represent the document for retreiving the most similar documents"` | The instruction for the query |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
import lancedb
|
||||||
|
from lancedb.pydantic import LanceModel, Vector
|
||||||
|
from lancedb.embeddings import get_registry, InstuctorEmbeddingFunction
|
||||||
|
|
||||||
|
instructor = get_registry().get("instructor").create(
|
||||||
|
source_instruction="represent the docuement for retreival",
|
||||||
|
query_instruction="represent the document for retreiving the most similar documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Schema(LanceModel):
|
||||||
|
vector: Vector(instructor.ndims()) = instructor.VectorField()
|
||||||
|
text: str = instructor.SourceField()
|
||||||
|
|
||||||
|
db = lancedb.connect("~/.lancedb")
|
||||||
|
tbl = db.create_table("test", schema=Schema, mode="overwrite")
|
||||||
|
|
||||||
|
texts = [{"text": "Capitalism has been dominant in the Western world since the end of feudalism, but most feel[who?] that..."},
|
||||||
|
{"text": "The disparate impact theory is especially controversial under the Fair Housing Act because the Act..."},
|
||||||
|
{"text": "Disparate impact in United States labor law refers to practices in employment, housing, and other areas that.."}]
|
||||||
|
|
||||||
|
tbl.add(texts)
|
||||||
|
```
|
||||||
|
|
||||||
## Multi-modal embedding functions
|
## Multi-modal embedding functions
|
||||||
Multi-modal embedding functions allow you query your table using both images and text.
|
Multi-modal embedding functions allow you query your table using both images and text.
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,19 @@ query_image = Image.open(p)
|
|||||||
table.search(query_image)
|
table.search(query_image)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
### Rate limit Handling
|
||||||
|
`EmbeddingFunction` class wraps the calls for source and query embedding generation inside a rate limit handler that retries the requests with exponential backoff after successive failures. By default the maximum retires is set to 7. You can tune it by setting it to a different number or disable it by setting it to 0.
|
||||||
|
Example
|
||||||
|
----
|
||||||
|
|
||||||
|
```python
|
||||||
|
clip = registry.get("open-clip").create() # Defaults to 7 max retries
|
||||||
|
clip = registry.get("open-clip").create(max_retries=10) # Increase max retries to 10
|
||||||
|
clip = registry.get("open-clip").create(max_retries=0) # Retries disabled
|
||||||
|
````
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
Embedding functions can also fail due to other errors that have nothing to do with rate limits. This is why the error is also logged.
|
||||||
|
|
||||||
### A little fun with PyDantic
|
### A little fun with PyDantic
|
||||||
LanceDB is integrated with PyDantic. Infact we've used the integration in the above example to define the schema. It is also being used behing the scene by the embdding function API to ingest useful information as table metadata.
|
LanceDB is integrated with PyDantic. Infact we've used the integration in the above example to define the schema. It is also being used behing the scene by the embdding function API to ingest useful information as table metadata.
|
||||||
|
|||||||
165
docs/src/examples/image_embeddings_roboflow.md
Normal file
165
docs/src/examples/image_embeddings_roboflow.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# How to Load Image Embeddings into LanceDB
|
||||||
|
|
||||||
|
With the rise of Large Multimodal Models (LMMs) such as [GPT-4 Vision](https://blog.roboflow.com/gpt-4-vision/), the need for storing image embeddings is growing. The most effective way to store text and image embeddings is in a vector database such as LanceDB. Vector databases are a special kind of data store that enables efficient search over stored embeddings.
|
||||||
|
|
||||||
|
[CLIP](https://blog.roboflow.com/openai-clip/), a multimodal model developed by OpenAI, is commonly used to calculate image embeddings. These embeddings can then be used with a vector database to build a semantic search engine that you can query using images or text. For example, you could use LanceDB and CLIP embeddings to build a search engine for a database of folders.
|
||||||
|
|
||||||
|
In this guide, we are going to show you how to use Roboflow Inference to load image embeddings into LanceDB. Without further ado, let’s get started!
|
||||||
|
|
||||||
|
## Step #1: Install Roboflow Inference
|
||||||
|
|
||||||
|
[Roboflow Inference](https://inference.roboflow.com) enables you to run state-of-the-art computer vision models with minimal configuration. Inference supports a range of models, from fine-tuned object detection, classification, and segmentation models to foundation models like CLIP. We will use Inference to calculate CLIP image embeddings.
|
||||||
|
|
||||||
|
Inference provides a HTTP API through which you can run vision models.
|
||||||
|
|
||||||
|
Inference powers the Roboflow hosted API, and is available as an open source utility. In this guide, we are going to run Inference locally, which enables you to calculate CLIP embeddings on your own hardware. We will also show you how to use the hosted Roboflow CLIP API, which is ideal if you need to scale and do not want to manage a system for calculating embeddings.
|
||||||
|
|
||||||
|
To get started, first install the Inference CLI:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install inference-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, install Docker. Refer to the official Docker installation instructions for your operating system to get Docker set up. Once Docker is ready, you can start Inference using the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
inference server start
|
||||||
|
```
|
||||||
|
|
||||||
|
An Inference server will start running at ‘http://localhost:9001’.
|
||||||
|
|
||||||
|
## Step #2: Set Up a LanceDB Vector Database
|
||||||
|
|
||||||
|
Now that we have Inference running, we can set up a LanceDB vector database. You can run LanceDB in JavaScript and Python. For this guide, we will use the Python API. But, you can take the HTTP requests we make below and change them to JavaScript if required.
|
||||||
|
|
||||||
|
For this guide, we are going to search the [COCO 128 dataset](https://universe.roboflow.com/team-roboflow/coco-128), which contains a wide range of objects. The variability in objects present in this dataset makes it a good dataset to demonstrate the capabilities of vector search. If you want to use this dataset, you can download [COCO 128 from Roboflow Universe](https://universe.roboflow.com/team-roboflow/coco-128). With that said, you can search whatever folder of images you want.
|
||||||
|
|
||||||
|
Once you have a dataset ready, install LanceDB with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install lancedb
|
||||||
|
```
|
||||||
|
|
||||||
|
We also need to install a specific commit of `tantivy`, a dependency of the LanceDB full text search engine we will use later in this guide:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install tantivy@git+https://github.com/quickwit-oss/tantivy-py#164adc87e1a033117001cf70e38c82a53014d985
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new Python file and add the following code:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import cv2
|
||||||
|
import supervision as sv
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import lancedb
|
||||||
|
|
||||||
|
db = lancedb.connect("./embeddings")
|
||||||
|
|
||||||
|
IMAGE_DIR = "images/"
|
||||||
|
API_KEY = os.environ.get("ROBOFLOW_API_KEY")
|
||||||
|
SERVER_URL = "http://localhost:9001"
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, image in enumerate(os.listdir(IMAGE_DIR)):
|
||||||
|
infer_clip_payload = {
|
||||||
|
#Images can be provided as urls or as base64 encoded strings
|
||||||
|
"image": {
|
||||||
|
"type": "base64",
|
||||||
|
"value": base64.b64encode(open(IMAGE_DIR + image, "rb").read()).decode("utf-8"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res = requests.post(
|
||||||
|
f"{SERVER_URL}/clip/embed_image?api_key={API_KEY}",
|
||||||
|
json=infer_clip_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
embeddings = res.json()['embeddings']
|
||||||
|
|
||||||
|
print("Calculated embedding for image: ", image)
|
||||||
|
|
||||||
|
image = {"vector": embeddings[0], "name": os.path.join(IMAGE_DIR, image)}
|
||||||
|
|
||||||
|
results.append(image)
|
||||||
|
|
||||||
|
tbl = db.create_table("images", data=results)
|
||||||
|
|
||||||
|
tbl.create_fts_index("name")
|
||||||
|
```
|
||||||
|
|
||||||
|
To use the code above, you will need a Roboflow API key. [Learn how to retrieve a Roboflow API key](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key). Run the following command to set up your API key in your environment:
|
||||||
|
|
||||||
|
```
|
||||||
|
export ROBOFLOW_API_KEY=""
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `IMAGE_DIR` value with the folder in which you are storing the images for which you want to calculate embeddings. If you want to use the Roboflow CLIP API to calculate embeddings, replace the `SERVER_URL` value with `https://infer.roboflow.com`.
|
||||||
|
|
||||||
|
Run the script above to create a new LanceDB database. This database will be stored on your local machine. The database will be called `embeddings` and the table will be called `images`.
|
||||||
|
|
||||||
|
The script above calculates all embeddings for a folder then creates a new table. To add additional images, use the following code:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def make_batches():
|
||||||
|
for i in range(5):
|
||||||
|
yield [
|
||||||
|
{"vector": [3.1, 4.1], "name": "image1.png"},
|
||||||
|
{"vector": [5.9, 26.5], "name": "image2.png"}
|
||||||
|
]
|
||||||
|
|
||||||
|
tbl = db.open_table("images")
|
||||||
|
tbl.add(make_batches())
|
||||||
|
```
|
||||||
|
|
||||||
|
Replacing the `make_batches()` function with code to load embeddings for images.
|
||||||
|
|
||||||
|
## Step #3: Run a Search Query
|
||||||
|
|
||||||
|
We are now ready to run a search query. To run a search query, we need a text embedding that represents a text query. We can use this embedding to search our LanceDB database for an entry.
|
||||||
|
|
||||||
|
Let’s calculate a text embedding for the query “cat”, then run a search query:
|
||||||
|
|
||||||
|
```python
|
||||||
|
infer_clip_payload = {
|
||||||
|
"text": "cat",
|
||||||
|
}
|
||||||
|
|
||||||
|
res = requests.post(
|
||||||
|
f"{SERVER_URL}/clip/embed_text?api_key={API_KEY}",
|
||||||
|
json=infer_clip_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
embeddings = res.json()['embeddings']
|
||||||
|
|
||||||
|
df = tbl.search(embeddings[0]).limit(3).to_list()
|
||||||
|
|
||||||
|
print("Results:")
|
||||||
|
|
||||||
|
for i in df:
|
||||||
|
print(i["name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
This code will search for the three images most closely related to the prompt “cat”. The names of the most similar three images will be printed to the console. Here are the three top results:
|
||||||
|
|
||||||
|
```
|
||||||
|
dataset/images/train/000000000650_jpg.rf.1b74ba165c5a3513a3211d4a80b69e1c.jpg
|
||||||
|
dataset/images/train/000000000138_jpg.rf.af439ef1c55dd8a4e4b142d186b9c957.jpg
|
||||||
|
dataset/images/train/000000000165_jpg.rf.eae14d5509bf0c9ceccddbb53a5f0c66.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Let’s open the top image:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The top image was a cat. Our search was successful.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
LanceDB is a vector database that you can use to store and efficiently search your image embeddings. You can use Roboflow Inference, a scalable computer vision inference server, to calculate CLIP embeddings that you can store in LanceDB.
|
||||||
|
|
||||||
|
You can use Inference and LanceDB together to build a range of applications with image embeddings, from a media search engine to a retrieval-augmented generation pipeline for use with LMMs.
|
||||||
|
|
||||||
|
To learn more about Inference and its capabilities, refer to the Inference documentation.
|
||||||
@@ -22,8 +22,6 @@ pip install lancedb
|
|||||||
|
|
||||||
::: lancedb.query.LanceQueryBuilder
|
::: lancedb.query.LanceQueryBuilder
|
||||||
|
|
||||||
::: lancedb.query.LanceFtsQueryBuilder
|
|
||||||
|
|
||||||
## Embeddings
|
## Embeddings
|
||||||
|
|
||||||
::: lancedb.embeddings.registry.EmbeddingFunctionRegistry
|
::: lancedb.embeddings.registry.EmbeddingFunctionRegistry
|
||||||
@@ -56,7 +54,7 @@ pip install lancedb
|
|||||||
|
|
||||||
## Utilities
|
## Utilities
|
||||||
|
|
||||||
::: lancedb.vector
|
::: lancedb.schema.vector
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
18
docs/src/python/saas-python.md
Normal file
18
docs/src/python/saas-python.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# LanceDB Python API Reference
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install lancedb
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection
|
||||||
|
|
||||||
|
::: lancedb.connect
|
||||||
|
|
||||||
|
::: lancedb.remote.db.RemoteDBConnection
|
||||||
|
|
||||||
|
## Table
|
||||||
|
|
||||||
|
::: lancedb.remote.table.RemoteTable
|
||||||
|
|
||||||
1
docs/src/robots.txt
Normal file
1
docs/src/robots.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
User-agent: *
|
||||||
@@ -18,28 +18,44 @@ python_file = ".py"
|
|||||||
python_folder = "python"
|
python_folder = "python"
|
||||||
|
|
||||||
files = glob.glob(glob_string, recursive=True)
|
files = glob.glob(glob_string, recursive=True)
|
||||||
excluded_files = [f for excluded_glob in excluded_globs for f in glob.glob(excluded_glob, recursive=True)]
|
excluded_files = [
|
||||||
|
f
|
||||||
|
for excluded_glob in excluded_globs
|
||||||
|
for f in glob.glob(excluded_glob, recursive=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def yield_lines(lines: Iterator[str], prefix: str, suffix: str):
|
def yield_lines(lines: Iterator[str], prefix: str, suffix: str):
|
||||||
in_code_block = False
|
in_code_block = False
|
||||||
# Python code has strict indentation
|
# Python code has strict indentation
|
||||||
strip_length = 0
|
strip_length = 0
|
||||||
|
skip_test = False
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
if "skip-test" in line:
|
||||||
|
skip_test = True
|
||||||
if line.strip().startswith(prefix + python_prefix):
|
if line.strip().startswith(prefix + python_prefix):
|
||||||
in_code_block = True
|
in_code_block = True
|
||||||
strip_length = len(line) - len(line.lstrip())
|
strip_length = len(line) - len(line.lstrip())
|
||||||
elif in_code_block and line.strip().startswith(suffix):
|
elif in_code_block and line.strip().startswith(suffix):
|
||||||
in_code_block = False
|
in_code_block = False
|
||||||
yield "\n"
|
if not skip_test:
|
||||||
|
yield "\n"
|
||||||
|
skip_test = False
|
||||||
elif in_code_block:
|
elif in_code_block:
|
||||||
yield line[strip_length:]
|
if not skip_test:
|
||||||
|
yield line[strip_length:]
|
||||||
|
|
||||||
for file in filter(lambda file: file not in excluded_files, files):
|
for file in filter(lambda file: file not in excluded_files, files):
|
||||||
with open(file, "r") as f:
|
with open(file, "r") as f:
|
||||||
lines = list(yield_lines(iter(f), "```", "```"))
|
lines = list(yield_lines(iter(f), "```", "```"))
|
||||||
|
|
||||||
if len(lines) > 0:
|
if len(lines) > 0:
|
||||||
out_path = Path(python_folder) / Path(file).name.strip(".md") / (Path(file).name.strip(".md") + python_file)
|
print(lines)
|
||||||
|
out_path = (
|
||||||
|
Path(python_folder)
|
||||||
|
/ Path(file).name.strip(".md")
|
||||||
|
/ (Path(file).name.strip(".md") + python_file)
|
||||||
|
)
|
||||||
print(out_path)
|
print(out_path)
|
||||||
out_path.parent.mkdir(exist_ok=True, parents=True)
|
out_path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
with open(out_path, "w") as out:
|
with open(out_path, "w") as out:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ npm install vectordb
|
|||||||
|
|
||||||
This will download the appropriate native library for your platform. We currently
|
This will download the appropriate native library for your platform. We currently
|
||||||
support x86_64 Linux, aarch64 Linux, Intel MacOS, and ARM (M1/M2) MacOS. We do not
|
support x86_64 Linux, aarch64 Linux, Intel MacOS, and ARM (M1/M2) MacOS. We do not
|
||||||
yet support Windows or musl-based Linux (such as Alpine Linux).
|
yet support musl-based Linux (such as Alpine Linux).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
74
node/package-lock.json
generated
74
node/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vectordb",
|
"name": "vectordb",
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vectordb",
|
"name": "vectordb",
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64",
|
"x64",
|
||||||
"arm64"
|
"arm64"
|
||||||
@@ -53,11 +53,11 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@lancedb/vectordb-darwin-arm64": "0.3.3",
|
"@lancedb/vectordb-darwin-arm64": "0.3.9",
|
||||||
"@lancedb/vectordb-darwin-x64": "0.3.3",
|
"@lancedb/vectordb-darwin-x64": "0.3.9",
|
||||||
"@lancedb/vectordb-linux-arm64-gnu": "0.3.3",
|
"@lancedb/vectordb-linux-arm64-gnu": "0.3.9",
|
||||||
"@lancedb/vectordb-linux-x64-gnu": "0.3.3",
|
"@lancedb/vectordb-linux-x64-gnu": "0.3.9",
|
||||||
"@lancedb/vectordb-win32-x64-msvc": "0.3.3"
|
"@lancedb/vectordb-win32-x64-msvc": "0.3.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@apache-arrow/ts": {
|
"node_modules/@apache-arrow/ts": {
|
||||||
@@ -317,9 +317,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lancedb/vectordb-darwin-arm64": {
|
"node_modules/@lancedb/vectordb-darwin-arm64": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-arm64/-/vectordb-darwin-arm64-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-arm64/-/vectordb-darwin-arm64-0.3.9.tgz",
|
||||||
"integrity": "sha512-nvyj7xNX2/wb/PH5TjyhLR/NQ1jVuoBw2B5UaSg7qf8Tnm5SSXWQ7F25RVKcKwh72fz1qB+CWW24ftZnRzbT/Q==",
|
"integrity": "sha512-irtAdfSRQDcfnMnB8T7D0atLFfu1MMZZ1JaxMKu24DDZ8e4IMYKUplxwvWni3241yA9yDE/pliRZCNQbQCEfrg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -329,9 +329,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@lancedb/vectordb-darwin-x64": {
|
"node_modules/@lancedb/vectordb-darwin-x64": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.3.9.tgz",
|
||||||
"integrity": "sha512-7CW+nILyPHp6cua0Rl0xaTDWw/vajEn/jCsEjFYgDmE+rtf5Z5Fum41FxR9C2TtIAvUK+nWb5mkYeOLqU6vRvg==",
|
"integrity": "sha512-4xXQoPheyIl1P5kRoKmZtaAHFrYdL9pw5yq+r6ewIx0TCemN4LSvzSUTqM5nZl3QPU8FeL0CGD8Gt2gMU0HQ2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -341,9 +341,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@lancedb/vectordb-linux-arm64-gnu": {
|
"node_modules/@lancedb/vectordb-linux-arm64-gnu": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.3.9.tgz",
|
||||||
"integrity": "sha512-MmhwbacKxZPkLwwOqysVY8mUb8lFoyFIPlYhSLV4xS1C8X4HWALljIul1qMl1RYudp9Uc3PsOzRexl+OvCGfUw==",
|
"integrity": "sha512-WIxCZKnLeSlz0PGURtKSX6hJ4CYE2o5P+IFmmuWOWB1uNapQu6zOpea6rNxcRFHUA0IJdO02lVxVfn2hDX4SMg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -353,9 +353,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@lancedb/vectordb-linux-x64-gnu": {
|
"node_modules/@lancedb/vectordb-linux-x64-gnu": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.3.9.tgz",
|
||||||
"integrity": "sha512-OrNlsKi/QPw59Po040oRKn8IuqFEk4upc/4FaFKqVkcmQjjZrMg5Kgy9ZfWIhHdAnWXXggZZIPArpt0X1B0ceA==",
|
"integrity": "sha512-bQbcV9adKzYbJLNzDjk9OYsMnT2IjmieLfb4IQ1hj5IUoWfbg80Bd0+gZUnrmrhG6fe56TIriFZYQR9i7TSE9Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -365,9 +365,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@lancedb/vectordb-win32-x64-msvc": {
|
"node_modules/@lancedb/vectordb-win32-x64-msvc": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.3.9.tgz",
|
||||||
"integrity": "sha512-lIT0A7a6eqX51IfGyhECtpXXgsr//kgbd+HZbcCdPy2GMmNezSch/7V22zExDSpF32hX8WfgcTLYCVWVilggDQ==",
|
"integrity": "sha512-7EXI7P1QvAfgJNPWWBMDOkoJ696gSBAClcyEJNYg0JV21jVFZRwJVI3bZXflesWduFi/mTuzPkFFA68us1u19A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -4869,33 +4869,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@lancedb/vectordb-darwin-arm64": {
|
"@lancedb/vectordb-darwin-arm64": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-arm64/-/vectordb-darwin-arm64-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-arm64/-/vectordb-darwin-arm64-0.3.9.tgz",
|
||||||
"integrity": "sha512-nvyj7xNX2/wb/PH5TjyhLR/NQ1jVuoBw2B5UaSg7qf8Tnm5SSXWQ7F25RVKcKwh72fz1qB+CWW24ftZnRzbT/Q==",
|
"integrity": "sha512-irtAdfSRQDcfnMnB8T7D0atLFfu1MMZZ1JaxMKu24DDZ8e4IMYKUplxwvWni3241yA9yDE/pliRZCNQbQCEfrg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@lancedb/vectordb-darwin-x64": {
|
"@lancedb/vectordb-darwin-x64": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.3.9.tgz",
|
||||||
"integrity": "sha512-7CW+nILyPHp6cua0Rl0xaTDWw/vajEn/jCsEjFYgDmE+rtf5Z5Fum41FxR9C2TtIAvUK+nWb5mkYeOLqU6vRvg==",
|
"integrity": "sha512-4xXQoPheyIl1P5kRoKmZtaAHFrYdL9pw5yq+r6ewIx0TCemN4LSvzSUTqM5nZl3QPU8FeL0CGD8Gt2gMU0HQ2A==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@lancedb/vectordb-linux-arm64-gnu": {
|
"@lancedb/vectordb-linux-arm64-gnu": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.3.9.tgz",
|
||||||
"integrity": "sha512-MmhwbacKxZPkLwwOqysVY8mUb8lFoyFIPlYhSLV4xS1C8X4HWALljIul1qMl1RYudp9Uc3PsOzRexl+OvCGfUw==",
|
"integrity": "sha512-WIxCZKnLeSlz0PGURtKSX6hJ4CYE2o5P+IFmmuWOWB1uNapQu6zOpea6rNxcRFHUA0IJdO02lVxVfn2hDX4SMg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@lancedb/vectordb-linux-x64-gnu": {
|
"@lancedb/vectordb-linux-x64-gnu": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.3.9.tgz",
|
||||||
"integrity": "sha512-OrNlsKi/QPw59Po040oRKn8IuqFEk4upc/4FaFKqVkcmQjjZrMg5Kgy9ZfWIhHdAnWXXggZZIPArpt0X1B0ceA==",
|
"integrity": "sha512-bQbcV9adKzYbJLNzDjk9OYsMnT2IjmieLfb4IQ1hj5IUoWfbg80Bd0+gZUnrmrhG6fe56TIriFZYQR9i7TSE9Q==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@lancedb/vectordb-win32-x64-msvc": {
|
"@lancedb/vectordb-win32-x64-msvc": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.3.9.tgz",
|
||||||
"integrity": "sha512-lIT0A7a6eqX51IfGyhECtpXXgsr//kgbd+HZbcCdPy2GMmNezSch/7V22zExDSpF32hX8WfgcTLYCVWVilggDQ==",
|
"integrity": "sha512-7EXI7P1QvAfgJNPWWBMDOkoJ696gSBAClcyEJNYg0JV21jVFZRwJVI3bZXflesWduFi/mTuzPkFFA68us1u19A==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@neon-rs/cli": {
|
"@neon-rs/cli": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vectordb",
|
"name": "vectordb",
|
||||||
"version": "0.3.3",
|
"version": "0.3.9",
|
||||||
"description": " Serverless, low-latency vector database for AI applications",
|
"description": " Serverless, low-latency vector database for AI applications",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@@ -81,10 +81,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@lancedb/vectordb-darwin-arm64": "0.3.3",
|
"@lancedb/vectordb-darwin-arm64": "0.3.9",
|
||||||
"@lancedb/vectordb-darwin-x64": "0.3.3",
|
"@lancedb/vectordb-darwin-x64": "0.3.9",
|
||||||
"@lancedb/vectordb-linux-arm64-gnu": "0.3.3",
|
"@lancedb/vectordb-linux-arm64-gnu": "0.3.9",
|
||||||
"@lancedb/vectordb-linux-x64-gnu": "0.3.3",
|
"@lancedb/vectordb-linux-x64-gnu": "0.3.9",
|
||||||
"@lancedb/vectordb-win32-x64-msvc": "0.3.3"
|
"@lancedb/vectordb-win32-x64-msvc": "0.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import type { EmbeddingFunction } from './embedding/embedding_function'
|
|||||||
import { RemoteConnection } from './remote'
|
import { RemoteConnection } from './remote'
|
||||||
import { Query } from './query'
|
import { Query } from './query'
|
||||||
import { isEmbeddingFunction } from './embedding/embedding_function'
|
import { isEmbeddingFunction } from './embedding/embedding_function'
|
||||||
|
import { type Literal, toSQL } from './util'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { databaseNew, databaseTableNames, databaseOpenTable, databaseDropTable, tableCreate, tableAdd, tableCreateVectorIndex, tableCountRows, tableDelete, tableCleanupOldVersions, tableCompactFiles } = require('../native.js')
|
const { databaseNew, databaseTableNames, databaseOpenTable, databaseDropTable, tableCreate, tableAdd, tableCreateVectorIndex, tableCountRows, tableDelete, tableUpdate, tableCleanupOldVersions, tableCompactFiles, tableListIndices, tableIndexStats } = require('../native.js')
|
||||||
|
|
||||||
export { Query }
|
export { Query }
|
||||||
export type { EmbeddingFunction }
|
export type { EmbeddingFunction }
|
||||||
@@ -260,6 +261,88 @@ export interface Table<T = number[]> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
delete: (filter: string) => Promise<void>
|
delete: (filter: string) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update rows in this table.
|
||||||
|
*
|
||||||
|
* This can be used to update a single row, many rows, all rows, or
|
||||||
|
* sometimes no rows (if your predicate matches nothing).
|
||||||
|
*
|
||||||
|
* @param args see {@link UpdateArgs} and {@link UpdateSqlArgs} for more details
|
||||||
|
*
|
||||||
|
* @examples
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const con = await lancedb.connect("./.lancedb")
|
||||||
|
* const data = [
|
||||||
|
* {id: 1, vector: [3, 3], name: 'Ye'},
|
||||||
|
* {id: 2, vector: [4, 4], name: 'Mike'},
|
||||||
|
* ];
|
||||||
|
* const tbl = await con.createTable("my_table", data)
|
||||||
|
*
|
||||||
|
* await tbl.update({
|
||||||
|
* filter: "id = 2",
|
||||||
|
* updates: { vector: [2, 2], name: "Michael" },
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* let results = await tbl.search([1, 1]).execute();
|
||||||
|
* // Returns [
|
||||||
|
* // {id: 2, vector: [2, 2], name: 'Michael'}
|
||||||
|
* // {id: 1, vector: [3, 3], name: 'Ye'}
|
||||||
|
* // ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
update: (args: UpdateArgs | UpdateSqlArgs) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the indicies on this table.
|
||||||
|
*/
|
||||||
|
listIndices: () => Promise<VectorIndex[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about an index.
|
||||||
|
*/
|
||||||
|
indexStats: (indexUuid: string) => Promise<IndexStats>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateArgs {
|
||||||
|
/**
|
||||||
|
* A filter in the same format used by a sql WHERE clause. The filter may be empty,
|
||||||
|
* in which case all rows will be updated.
|
||||||
|
*/
|
||||||
|
where?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key-value map of updates. The keys are the column names, and the values are the
|
||||||
|
* new values to set
|
||||||
|
*/
|
||||||
|
values: Record<string, Literal>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSqlArgs {
|
||||||
|
/**
|
||||||
|
* A filter in the same format used by a sql WHERE clause. The filter may be empty,
|
||||||
|
* in which case all rows will be updated.
|
||||||
|
*/
|
||||||
|
where?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key-value map of updates. The keys are the column names, and the values are the
|
||||||
|
* new values to set as SQL expressions.
|
||||||
|
*/
|
||||||
|
valuesSql: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorIndex {
|
||||||
|
columns: string[]
|
||||||
|
name: string
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexStats {
|
||||||
|
numIndexedRows: number | null
|
||||||
|
numUnindexedRows: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,6 +488,16 @@ export class LocalTable<T = number[]> implements Table<T> {
|
|||||||
return new Query(query, this._tbl, this._embeddings)
|
return new Query(query, this._tbl, this._embeddings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a filter query to find all rows matching the specified criteria
|
||||||
|
* @param value The filter criteria (like SQL where clause syntax)
|
||||||
|
*/
|
||||||
|
filter (value: string): Query<T> {
|
||||||
|
return new Query(undefined, this._tbl, this._embeddings).filter(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
where = this.filter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert records into this Table.
|
* Insert records into this Table.
|
||||||
*
|
*
|
||||||
@@ -460,6 +553,31 @@ export class LocalTable<T = number[]> implements Table<T> {
|
|||||||
return tableDelete.call(this._tbl, filter).then((newTable: any) => { this._tbl = newTable })
|
return tableDelete.call(this._tbl, filter).then((newTable: any) => { this._tbl = newTable })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update rows in this table.
|
||||||
|
*
|
||||||
|
* @param args see {@link UpdateArgs} and {@link UpdateSqlArgs} for more details
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async update (args: UpdateArgs | UpdateSqlArgs): Promise<void> {
|
||||||
|
let filter: string | null
|
||||||
|
let updates: Record<string, string>
|
||||||
|
|
||||||
|
if ('valuesSql' in args) {
|
||||||
|
filter = args.where ?? null
|
||||||
|
updates = args.valuesSql
|
||||||
|
} else {
|
||||||
|
filter = args.where ?? null
|
||||||
|
updates = {}
|
||||||
|
for (const [key, value] of Object.entries(args.values)) {
|
||||||
|
updates[key] = toSQL(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableUpdate.call(this._tbl, filter, updates).then((newTable: any) => { this._tbl = newTable })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up old versions of the table, freeing disk space.
|
* Clean up old versions of the table, freeing disk space.
|
||||||
*
|
*
|
||||||
@@ -502,6 +620,14 @@ export class LocalTable<T = number[]> implements Table<T> {
|
|||||||
return res.metrics
|
return res.metrics
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listIndices (): Promise<VectorIndex[]> {
|
||||||
|
return tableListIndices.call(this._tbl)
|
||||||
|
}
|
||||||
|
|
||||||
|
async indexStats (indexUuid: string): Promise<IndexStats> {
|
||||||
|
return tableIndexStats.call(this._tbl, indexUuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CleanupStats {
|
export interface CleanupStats {
|
||||||
|
|||||||
@@ -23,27 +23,29 @@ const { tableSearch } = require('../native.js')
|
|||||||
* A builder for nearest neighbor queries for LanceDB.
|
* A builder for nearest neighbor queries for LanceDB.
|
||||||
*/
|
*/
|
||||||
export class Query<T = number[]> {
|
export class Query<T = number[]> {
|
||||||
private readonly _query: T
|
private readonly _query?: T
|
||||||
private readonly _tbl?: any
|
private readonly _tbl?: any
|
||||||
private _queryVector?: number[]
|
private _queryVector?: number[]
|
||||||
private _limit: number
|
private _limit?: number
|
||||||
private _refineFactor?: number
|
private _refineFactor?: number
|
||||||
private _nprobes: number
|
private _nprobes: number
|
||||||
private _select?: string[]
|
private _select?: string[]
|
||||||
private _filter?: string
|
private _filter?: string
|
||||||
private _metricType?: MetricType
|
private _metricType?: MetricType
|
||||||
|
private _prefilter: boolean
|
||||||
protected readonly _embeddings?: EmbeddingFunction<T>
|
protected readonly _embeddings?: EmbeddingFunction<T>
|
||||||
|
|
||||||
constructor (query: T, tbl?: any, embeddings?: EmbeddingFunction<T>) {
|
constructor (query?: T, tbl?: any, embeddings?: EmbeddingFunction<T>) {
|
||||||
this._tbl = tbl
|
this._tbl = tbl
|
||||||
this._query = query
|
this._query = query
|
||||||
this._limit = 10
|
this._limit = undefined
|
||||||
this._nprobes = 20
|
this._nprobes = 20
|
||||||
this._refineFactor = undefined
|
this._refineFactor = undefined
|
||||||
this._select = undefined
|
this._select = undefined
|
||||||
this._filter = undefined
|
this._filter = undefined
|
||||||
this._metricType = undefined
|
this._metricType = undefined
|
||||||
this._embeddings = embeddings
|
this._embeddings = embeddings
|
||||||
|
this._prefilter = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@@ -102,14 +104,21 @@ export class Query<T = number[]> {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefilter (value: boolean): Query<T> {
|
||||||
|
this._prefilter = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the query and return the results as an Array of Objects
|
* Execute the query and return the results as an Array of Objects
|
||||||
*/
|
*/
|
||||||
async execute<T = Record<string, unknown>> (): Promise<T[]> {
|
async execute<T = Record<string, unknown>> (): Promise<T[]> {
|
||||||
if (this._embeddings !== undefined) {
|
if (this._query !== undefined) {
|
||||||
this._queryVector = (await this._embeddings.embed([this._query]))[0]
|
if (this._embeddings !== undefined) {
|
||||||
} else {
|
this._queryVector = (await this._embeddings.embed([this._query]))[0]
|
||||||
this._queryVector = this._query as number[]
|
} else {
|
||||||
|
this._queryVector = this._query as number[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isElectron = this.isElectron()
|
const isElectron = this.isElectron()
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class HttpLancedbClient {
|
|||||||
vector: number[],
|
vector: number[],
|
||||||
k: number,
|
k: number,
|
||||||
nprobes: number,
|
nprobes: number,
|
||||||
|
prefilter: boolean,
|
||||||
refineFactor?: number,
|
refineFactor?: number,
|
||||||
columns?: string[],
|
columns?: string[],
|
||||||
filter?: string
|
filter?: string
|
||||||
@@ -50,7 +51,8 @@ export class HttpLancedbClient {
|
|||||||
nprobes,
|
nprobes,
|
||||||
refineFactor,
|
refineFactor,
|
||||||
columns,
|
columns,
|
||||||
filter
|
filter,
|
||||||
|
prefilter
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -63,6 +65,9 @@ export class HttpLancedbClient {
|
|||||||
}
|
}
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
console.error('error: ', err)
|
console.error('error: ', err)
|
||||||
|
if (err.response === undefined) {
|
||||||
|
throw new Error(`Network Error: ${err.message as string}`)
|
||||||
|
}
|
||||||
return err.response
|
return err.response
|
||||||
})
|
})
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
@@ -86,13 +91,17 @@ export class HttpLancedbClient {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-api-key': this._apiKey()
|
'x-api-key': this._apiKey(),
|
||||||
|
...(this._dbName !== undefined ? { 'x-lancedb-database': this._dbName } : {})
|
||||||
},
|
},
|
||||||
params,
|
params,
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
}
|
}
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
console.error('error: ', err)
|
console.error('error: ', err)
|
||||||
|
if (err.response === undefined) {
|
||||||
|
throw new Error(`Network Error: ${err.message as string}`)
|
||||||
|
}
|
||||||
return err.response
|
return err.response
|
||||||
})
|
})
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
@@ -128,6 +137,9 @@ export class HttpLancedbClient {
|
|||||||
}
|
}
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
console.error('error: ', err)
|
console.error('error: ', err)
|
||||||
|
if (err.response === undefined) {
|
||||||
|
throw new Error(`Network Error: ${err.message as string}`)
|
||||||
|
}
|
||||||
return err.response
|
return err.response
|
||||||
})
|
})
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type EmbeddingFunction, type Table, type VectorIndexParams, type Connection,
|
type EmbeddingFunction, type Table, type VectorIndexParams, type Connection,
|
||||||
type ConnectionOptions, type CreateTableOptions, type WriteOptions
|
type ConnectionOptions, type CreateTableOptions, type VectorIndex,
|
||||||
|
type WriteOptions,
|
||||||
|
type IndexStats,
|
||||||
|
type UpdateArgs, type UpdateSqlArgs
|
||||||
} from '../index'
|
} from '../index'
|
||||||
import { Query } from '../query'
|
import { Query } from '../query'
|
||||||
|
|
||||||
@@ -152,6 +155,7 @@ export class RemoteQuery<T = number[]> extends Query<T> {
|
|||||||
queryVector,
|
queryVector,
|
||||||
(this as any)._limit,
|
(this as any)._limit,
|
||||||
(this as any)._nprobes,
|
(this as any)._nprobes,
|
||||||
|
(this as any)._prefilter,
|
||||||
(this as any)._refineFactor,
|
(this as any)._refineFactor,
|
||||||
(this as any)._select,
|
(this as any)._select,
|
||||||
(this as any)._filter
|
(this as any)._filter
|
||||||
@@ -235,10 +239,32 @@ export class RemoteTable<T = number[]> implements Table<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async countRows (): Promise<number> {
|
async countRows (): Promise<number> {
|
||||||
throw new Error('Not implemented')
|
const result = await this._client.post(`/v1/table/${this._name}/describe/`)
|
||||||
|
return result.data?.stats?.num_rows
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete (filter: string): Promise<void> {
|
async delete (filter: string): Promise<void> {
|
||||||
await this._client.post(`/v1/table/${this._name}/delete/`, { predicate: filter })
|
await this._client.post(`/v1/table/${this._name}/delete/`, { predicate: filter })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update (args: UpdateArgs | UpdateSqlArgs): Promise<void> {
|
||||||
|
throw new Error('Not implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
async listIndices (): Promise<VectorIndex[]> {
|
||||||
|
const results = await this._client.post(`/v1/table/${this._name}/index/list/`)
|
||||||
|
return results.data.indexes?.map((index: any) => ({
|
||||||
|
columns: index.columns,
|
||||||
|
name: index.index_name,
|
||||||
|
uuid: index.index_uuid
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async indexStats (indexUuid: string): Promise<IndexStats> {
|
||||||
|
const results = await this._client.post(`/v1/table/${this._name}/index/${indexUuid}/stats/`)
|
||||||
|
return {
|
||||||
|
numIndexedRows: results.data.num_indexed_rows,
|
||||||
|
numUnindexedRows: results.data.num_unindexed_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,12 +78,31 @@ describe('LanceDB client', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('limits # of results', async function () {
|
it('limits # of results', async function () {
|
||||||
const uri = await createTestDB()
|
const uri = await createTestDB(2, 100)
|
||||||
const con = await lancedb.connect(uri)
|
const con = await lancedb.connect(uri)
|
||||||
const table = await con.openTable('vectors')
|
const table = await con.openTable('vectors')
|
||||||
const results = await table.search([0.1, 0.3]).limit(1).execute()
|
let results = await table.search([0.1, 0.3]).limit(1).execute()
|
||||||
assert.equal(results.length, 1)
|
assert.equal(results.length, 1)
|
||||||
assert.equal(results[0].id, 1)
|
assert.equal(results[0].id, 1)
|
||||||
|
|
||||||
|
// there is a default limit if unspecified
|
||||||
|
results = await table.search([0.1, 0.3]).execute()
|
||||||
|
assert.equal(results.length, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a filter / where clause without vector search', async function () {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
const assertResults = (results: Array<Record<string, unknown>>) => {
|
||||||
|
assert.equal(results.length, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = await createTestDB(2, 100)
|
||||||
|
const con = await lancedb.connect(uri)
|
||||||
|
const table = (await con.openTable('vectors')) as LocalTable
|
||||||
|
let results = await table.filter('id % 2 = 0').execute()
|
||||||
|
assertResults(results)
|
||||||
|
results = await table.where('id % 2 = 0').execute()
|
||||||
|
assertResults(results)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses a filter / where clause', async function () {
|
it('uses a filter / where clause', async function () {
|
||||||
@@ -102,6 +121,20 @@ describe('LanceDB client', function () {
|
|||||||
assertResults(results)
|
assertResults(results)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should correctly process prefilter/postfilter', async function () {
|
||||||
|
const uri = await createTestDB(16, 300)
|
||||||
|
const con = await lancedb.connect(uri)
|
||||||
|
const table = await con.openTable('vectors')
|
||||||
|
await table.createIndex({ type: 'ivf_pq', column: 'vector', num_partitions: 2, max_iters: 2, num_sub_vectors: 2 })
|
||||||
|
// post filter should return less than the limit
|
||||||
|
let results = await table.search(new Array(16).fill(0.1)).limit(10).filter('id >= 10').prefilter(false).execute()
|
||||||
|
assert.isTrue(results.length < 10)
|
||||||
|
|
||||||
|
// pre filter should return exactly the limit
|
||||||
|
results = await table.search(new Array(16).fill(0.1)).limit(10).filter('id >= 10').prefilter(true).execute()
|
||||||
|
assert.isTrue(results.length === 10)
|
||||||
|
})
|
||||||
|
|
||||||
it('select only a subset of columns', async function () {
|
it('select only a subset of columns', async function () {
|
||||||
const uri = await createTestDB()
|
const uri = await createTestDB()
|
||||||
const con = await lancedb.connect(uri)
|
const con = await lancedb.connect(uri)
|
||||||
@@ -246,6 +279,46 @@ describe('LanceDB client', function () {
|
|||||||
assert.equal(await table.countRows(), 2)
|
assert.equal(await table.countRows(), 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('can update records in the table', async function () {
|
||||||
|
const uri = await createTestDB()
|
||||||
|
const con = await lancedb.connect(uri)
|
||||||
|
|
||||||
|
const table = await con.openTable('vectors')
|
||||||
|
assert.equal(await table.countRows(), 2)
|
||||||
|
|
||||||
|
await table.update({ where: 'price = 10', valuesSql: { price: '100' } })
|
||||||
|
const results = await table.search([0.1, 0.2]).execute()
|
||||||
|
assert.equal(results[0].price, 100)
|
||||||
|
assert.equal(results[1].price, 11)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can update the records using a literal value', async function () {
|
||||||
|
const uri = await createTestDB()
|
||||||
|
const con = await lancedb.connect(uri)
|
||||||
|
|
||||||
|
const table = await con.openTable('vectors')
|
||||||
|
assert.equal(await table.countRows(), 2)
|
||||||
|
|
||||||
|
await table.update({ where: 'price = 10', values: { price: 100 } })
|
||||||
|
const results = await table.search([0.1, 0.2]).execute()
|
||||||
|
assert.equal(results[0].price, 100)
|
||||||
|
assert.equal(results[1].price, 11)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can update every record in the table', async function () {
|
||||||
|
const uri = await createTestDB()
|
||||||
|
const con = await lancedb.connect(uri)
|
||||||
|
|
||||||
|
const table = await con.openTable('vectors')
|
||||||
|
assert.equal(await table.countRows(), 2)
|
||||||
|
|
||||||
|
await table.update({ valuesSql: { price: '100' } })
|
||||||
|
const results = await table.search([0.1, 0.2]).execute()
|
||||||
|
|
||||||
|
assert.equal(results[0].price, 100)
|
||||||
|
assert.equal(results[1].price, 100)
|
||||||
|
})
|
||||||
|
|
||||||
it('can delete records from a table', async function () {
|
it('can delete records from a table', async function () {
|
||||||
const uri = await createTestDB()
|
const uri = await createTestDB()
|
||||||
const con = await lancedb.connect(uri)
|
const con = await lancedb.connect(uri)
|
||||||
@@ -282,7 +355,8 @@ describe('LanceDB client', function () {
|
|||||||
)
|
)
|
||||||
const table = await con.createTable({ name: 'vectors', schema })
|
const table = await con.createTable({ name: 'vectors', schema })
|
||||||
await table.add([{ vector: Array(128).fill(0.1) }])
|
await table.add([{ vector: Array(128).fill(0.1) }])
|
||||||
await table.delete('vector IS NOT NULL')
|
// https://github.com/lancedb/lance/issues/1635
|
||||||
|
await table.delete('true')
|
||||||
const result = await table.search(Array(128).fill(0.1)).execute()
|
const result = await table.search(Array(128).fill(0.1)).execute()
|
||||||
assert.isEmpty(result)
|
assert.isEmpty(result)
|
||||||
})
|
})
|
||||||
@@ -328,6 +402,24 @@ describe('LanceDB client', function () {
|
|||||||
const createIndex = table.createIndex({ type: 'ivf_pq', column: 'name', num_partitions: -1, max_iters: 2, num_sub_vectors: 2 })
|
const createIndex = table.createIndex({ type: 'ivf_pq', column: 'name', num_partitions: -1, max_iters: 2, num_sub_vectors: 2 })
|
||||||
await expect(createIndex).to.be.rejectedWith('num_partitions: must be > 0')
|
await expect(createIndex).to.be.rejectedWith('num_partitions: must be > 0')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should be able to list index and stats', async function () {
|
||||||
|
const uri = await createTestDB(32, 300)
|
||||||
|
const con = await lancedb.connect(uri)
|
||||||
|
const table = await con.openTable('vectors')
|
||||||
|
await table.createIndex({ type: 'ivf_pq', column: 'vector', num_partitions: 2, max_iters: 2, num_sub_vectors: 2 })
|
||||||
|
|
||||||
|
const indices = await table.listIndices()
|
||||||
|
expect(indices).to.have.lengthOf(1)
|
||||||
|
expect(indices[0].name).to.equal('vector_idx')
|
||||||
|
expect(indices[0].uuid).to.not.be.equal(undefined)
|
||||||
|
expect(indices[0].columns).to.have.lengthOf(1)
|
||||||
|
expect(indices[0].columns[0]).to.equal('vector')
|
||||||
|
|
||||||
|
const stats = await table.indexStats(indices[0].uuid)
|
||||||
|
expect(stats.numIndexedRows).to.equal(300)
|
||||||
|
expect(stats.numUnindexedRows).to.equal(0)
|
||||||
|
}).timeout(50_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when using a custom embedding function', function () {
|
describe('when using a custom embedding function', function () {
|
||||||
@@ -378,6 +470,40 @@ describe('LanceDB client', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Remote LanceDB client', function () {
|
||||||
|
describe('when the server is not reachable', function () {
|
||||||
|
it('produces a network error', async function () {
|
||||||
|
const con = await lancedb.connect({
|
||||||
|
uri: 'db://test-1234',
|
||||||
|
region: 'asdfasfasfdf',
|
||||||
|
apiKey: 'some-api-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
// GET
|
||||||
|
try {
|
||||||
|
await con.tableNames()
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.have.property('message', 'Network Error: getaddrinfo ENOTFOUND test-1234.asdfasfasfdf.api.lancedb.com')
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST
|
||||||
|
try {
|
||||||
|
await con.createTable({ name: 'vectors', schema: new Schema([]) })
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.have.property('message', 'Network Error: getaddrinfo ENOTFOUND test-1234.asdfasfasfdf.api.lancedb.com')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
const table = await con.openTable('vectors')
|
||||||
|
try {
|
||||||
|
await table.search([0.1, 0.3]).execute()
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.have.property('message', 'Network Error: getaddrinfo ENOTFOUND test-1234.asdfasfasfdf.api.lancedb.com')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Query object', function () {
|
describe('Query object', function () {
|
||||||
it('sets custom parameters', async function () {
|
it('sets custom parameters', async function () {
|
||||||
const query = new Query([0.1, 0.3])
|
const query = new Query([0.1, 0.3])
|
||||||
@@ -475,7 +601,7 @@ describe('Compact and cleanup', function () {
|
|||||||
|
|
||||||
// should have no effect, but this validates the arguments are parsed.
|
// should have no effect, but this validates the arguments are parsed.
|
||||||
await table.compactFiles({
|
await table.compactFiles({
|
||||||
targetRowsPerFragment: 1024 * 10,
|
targetRowsPerFragment: 102410,
|
||||||
maxRowsPerGroup: 1024,
|
maxRowsPerGroup: 1024,
|
||||||
materializeDeletions: true,
|
materializeDeletions: true,
|
||||||
materializeDeletionsThreshold: 0.5,
|
materializeDeletionsThreshold: 0.5,
|
||||||
|
|||||||
45
node/src/test/util.ts
Normal file
45
node/src/test/util.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2023 LanceDB Developers.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { toSQL } from '../util'
|
||||||
|
import * as chai from 'chai'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('toSQL', function () {
|
||||||
|
it('should turn string to SQL expression', function () {
|
||||||
|
expect(toSQL('foo')).to.equal("'foo'")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should turn number to SQL expression', function () {
|
||||||
|
expect(toSQL(123)).to.equal('123')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should turn boolean to SQL expression', function () {
|
||||||
|
expect(toSQL(true)).to.equal('TRUE')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should turn null to SQL expression', function () {
|
||||||
|
expect(toSQL(null)).to.equal('NULL')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should turn Date to SQL expression', function () {
|
||||||
|
const date = new Date('05 October 2011 14:48 UTC')
|
||||||
|
expect(toSQL(date)).to.equal("'2011-10-05T14:48:00.000Z'")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should turn array to SQL expression', function () {
|
||||||
|
expect(toSQL(['foo', 'bar', true, 1])).to.equal("['foo', 'bar', TRUE, 1]")
|
||||||
|
})
|
||||||
|
})
|
||||||
44
node/src/util.ts
Normal file
44
node/src/util.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2023 LanceDB Developers.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
export type Literal = string | number | boolean | null | Date | Literal[]
|
||||||
|
|
||||||
|
export function toSQL (value: Literal): string {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return `'${value}'`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? 'TRUE' : 'FALSE'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return 'NULL'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return `'${value.toISOString()}'`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map(toSQL).join(', ')}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unsupported value type: ${typeof value} value: (${value})`)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.3.2
|
current_version = 0.3.5
|
||||||
commit = True
|
commit = True
|
||||||
message = [python] Bump version: {current_version} → {new_version}
|
message = [python] Bump version: {current_version} → {new_version}
|
||||||
tag = True
|
tag = True
|
||||||
|
|||||||
@@ -16,17 +16,18 @@ from typing import Optional
|
|||||||
|
|
||||||
__version__ = importlib.metadata.version("lancedb")
|
__version__ = importlib.metadata.version("lancedb")
|
||||||
|
|
||||||
from .db import URI, DBConnection, LanceDBConnection
|
from .common import URI
|
||||||
|
from .db import DBConnection, LanceDBConnection
|
||||||
from .remote.db import RemoteDBConnection
|
from .remote.db import RemoteDBConnection
|
||||||
from .schema import vector
|
from .schema import vector # noqa: F401
|
||||||
from .utils import sentry_log
|
from .utils import sentry_log # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
def connect(
|
def connect(
|
||||||
uri: URI,
|
uri: URI,
|
||||||
*,
|
*,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
region: str = "us-west-2",
|
region: str = "us-east-1",
|
||||||
host_override: Optional[str] = None,
|
host_override: Optional[str] = None,
|
||||||
) -> DBConnection:
|
) -> DBConnection:
|
||||||
"""Connect to a LanceDB database.
|
"""Connect to a LanceDB database.
|
||||||
@@ -38,7 +39,7 @@ def connect(
|
|||||||
api_key: str, optional
|
api_key: str, optional
|
||||||
If presented, connect to LanceDB cloud.
|
If presented, connect to LanceDB cloud.
|
||||||
Otherwise, connect to a database on file system or cloud storage.
|
Otherwise, connect to a database on file system or cloud storage.
|
||||||
region: str, default "us-west-2"
|
region: str, default "us-east-1"
|
||||||
The region to use for LanceDB Cloud.
|
The region to use for LanceDB Cloud.
|
||||||
host_override: str, optional
|
host_override: str, optional
|
||||||
The override url for LanceDB Cloud.
|
The override url for LanceDB Cloud.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
@@ -38,3 +40,26 @@ class MockTextEmbeddingFunction(TextEmbeddingFunction):
|
|||||||
|
|
||||||
def ndims(self):
|
def ndims(self):
|
||||||
return 10
|
return 10
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitedAPI:
|
||||||
|
rate_limit = 0.1 # 1 request per 0.1 second
|
||||||
|
last_request_time = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_request():
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if current_time - RateLimitedAPI.last_request_time < RateLimitedAPI.rate_limit:
|
||||||
|
raise Exception("Rate limit exceeded. Please try again later.")
|
||||||
|
|
||||||
|
# Simulate a successful request
|
||||||
|
RateLimitedAPI.last_request_time = current_time
|
||||||
|
return "Request successful"
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register("test-rate-limited")
|
||||||
|
class MockRateLimitedEmbeddingFunction(MockTextEmbeddingFunction):
|
||||||
|
def generate_embeddings(self, texts):
|
||||||
|
RateLimitedAPI.make_request()
|
||||||
|
return [self._compute_one_embedding(row) for row in texts]
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ def contextualize(raw_df: "pd.DataFrame") -> Contextualizer:
|
|||||||
context windows that don't cross document boundaries. In this case, we can
|
context windows that don't cross document boundaries. In this case, we can
|
||||||
pass ``document_id`` as the group by.
|
pass ``document_id`` as the group by.
|
||||||
|
|
||||||
>>> contextualize(data).window(4).stride(2).text_col('token').groupby('document_id').to_pandas()
|
>>> (contextualize(data)
|
||||||
|
... .window(4).stride(2).text_col('token').groupby('document_id')
|
||||||
|
... .to_pandas())
|
||||||
token document_id
|
token document_id
|
||||||
0 The quick brown fox 1
|
0 The quick brown fox 1
|
||||||
2 brown fox jumped over 1
|
2 brown fox jumped over 1
|
||||||
@@ -92,18 +94,24 @@ def contextualize(raw_df: "pd.DataFrame") -> Contextualizer:
|
|||||||
6 the lazy dog 1
|
6 the lazy dog 1
|
||||||
9 I love sandwiches 2
|
9 I love sandwiches 2
|
||||||
|
|
||||||
``min_window_size`` determines the minimum size of the context windows that are generated
|
``min_window_size`` determines the minimum size of the context windows
|
||||||
This can be used to trim the last few context windows which have size less than
|
that are generated.This can be used to trim the last few context windows
|
||||||
``min_window_size``. By default context windows of size 1 are skipped.
|
which have size less than ``min_window_size``.
|
||||||
|
By default context windows of size 1 are skipped.
|
||||||
|
|
||||||
>>> contextualize(data).window(6).stride(3).text_col('token').groupby('document_id').to_pandas()
|
>>> (contextualize(data)
|
||||||
|
... .window(6).stride(3).text_col('token').groupby('document_id')
|
||||||
|
... .to_pandas())
|
||||||
token document_id
|
token document_id
|
||||||
0 The quick brown fox jumped over 1
|
0 The quick brown fox jumped over 1
|
||||||
3 fox jumped over the lazy dog 1
|
3 fox jumped over the lazy dog 1
|
||||||
6 the lazy dog 1
|
6 the lazy dog 1
|
||||||
9 I love sandwiches 2
|
9 I love sandwiches 2
|
||||||
|
|
||||||
>>> contextualize(data).window(6).stride(3).min_window_size(4).text_col('token').groupby('document_id').to_pandas()
|
>>> (contextualize(data)
|
||||||
|
... .window(6).stride(3).min_window_size(4).text_col('token')
|
||||||
|
... .groupby('document_id')
|
||||||
|
... .to_pandas())
|
||||||
token document_id
|
token document_id
|
||||||
0 The quick brown fox jumped over 1
|
0 The quick brown fox jumped over 1
|
||||||
3 fox jumped over the lazy dog 1
|
3 fox jumped over the lazy dog 1
|
||||||
@@ -113,7 +121,9 @@ def contextualize(raw_df: "pd.DataFrame") -> Contextualizer:
|
|||||||
|
|
||||||
|
|
||||||
class Contextualizer:
|
class Contextualizer:
|
||||||
"""Create context windows from a DataFrame. See [lancedb.context.contextualize][]."""
|
"""Create context windows from a DataFrame.
|
||||||
|
See [lancedb.context.contextualize][].
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, raw_df):
|
def __init__(self, raw_df):
|
||||||
self._text_col = None
|
self._text_col = None
|
||||||
@@ -183,7 +193,7 @@ class Contextualizer:
|
|||||||
deprecated_in="0.3.1",
|
deprecated_in="0.3.1",
|
||||||
removed_in="0.4.0",
|
removed_in="0.4.0",
|
||||||
current_version=__version__,
|
current_version=__version__,
|
||||||
details="Use the bar function instead",
|
details="Use to_pandas() instead",
|
||||||
)
|
)
|
||||||
def to_df(self) -> "pd.DataFrame":
|
def to_df(self) -> "pd.DataFrame":
|
||||||
return self.to_pandas()
|
return self.to_pandas()
|
||||||
|
|||||||
@@ -14,26 +14,39 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from abc import ABC, abstractmethod
|
from abc import abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Union
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Union
|
||||||
|
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
|
from overrides import EnforceOverrides, override
|
||||||
from pyarrow import fs
|
from pyarrow import fs
|
||||||
|
|
||||||
from .common import DATA, URI
|
|
||||||
from .embeddings import EmbeddingFunctionConfig
|
|
||||||
from .pydantic import LanceModel
|
|
||||||
from .table import LanceTable, Table
|
from .table import LanceTable, Table
|
||||||
from .util import fs_from_uri, get_uri_location, get_uri_scheme
|
from .util import fs_from_uri, get_uri_location, get_uri_scheme
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .common import DATA, URI
|
||||||
|
from .embeddings import EmbeddingFunctionConfig
|
||||||
|
from .pydantic import LanceModel
|
||||||
|
|
||||||
class DBConnection(ABC):
|
|
||||||
|
class DBConnection(EnforceOverrides):
|
||||||
"""An active LanceDB connection interface."""
|
"""An active LanceDB connection interface."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def table_names(self) -> list[str]:
|
def table_names(
|
||||||
"""List all table names in the database."""
|
self, page_token: Optional[str] = None, limit: int = 10
|
||||||
|
) -> Iterable[str]:
|
||||||
|
"""List all table in this database
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
page_token: str, optional
|
||||||
|
The token to use for pagination. If not present, start from the beginning.
|
||||||
|
limit: int, default 10
|
||||||
|
The size of the page to return.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -45,6 +58,7 @@ class DBConnection(ABC):
|
|||||||
mode: str = "create",
|
mode: str = "create",
|
||||||
on_bad_vectors: str = "error",
|
on_bad_vectors: str = "error",
|
||||||
fill_value: float = 0.0,
|
fill_value: float = 0.0,
|
||||||
|
embedding_functions: Optional[List[EmbeddingFunctionConfig]] = None,
|
||||||
) -> Table:
|
) -> Table:
|
||||||
"""Create a [Table][lancedb.table.Table] in the database.
|
"""Create a [Table][lancedb.table.Table] in the database.
|
||||||
|
|
||||||
@@ -52,12 +66,24 @@ class DBConnection(ABC):
|
|||||||
----------
|
----------
|
||||||
name: str
|
name: str
|
||||||
The name of the table.
|
The name of the table.
|
||||||
data: list, tuple, dict, pd.DataFrame; optional
|
data: The data to initialize the table, *optional*
|
||||||
The data to initialize the table. User must provide at least one of `data` or `schema`.
|
User must provide at least one of `data` or `schema`.
|
||||||
schema: pyarrow.Schema or LanceModel; optional
|
Acceptable types are:
|
||||||
The schema of the table.
|
|
||||||
|
- dict or list-of-dict
|
||||||
|
|
||||||
|
- pandas.DataFrame
|
||||||
|
|
||||||
|
- pyarrow.Table or pyarrow.RecordBatch
|
||||||
|
schema: The schema of the table, *optional*
|
||||||
|
Acceptable types are:
|
||||||
|
|
||||||
|
- pyarrow.Schema
|
||||||
|
|
||||||
|
- [LanceModel][lancedb.pydantic.LanceModel]
|
||||||
mode: str; default "create"
|
mode: str; default "create"
|
||||||
The mode to use when creating the table. Can be either "create" or "overwrite".
|
The mode to use when creating the table.
|
||||||
|
Can be either "create" or "overwrite".
|
||||||
By default, if the table already exists, an exception is raised.
|
By default, if the table already exists, an exception is raised.
|
||||||
If you want to overwrite the table, use mode="overwrite".
|
If you want to overwrite the table, use mode="overwrite".
|
||||||
on_bad_vectors: str, default "error"
|
on_bad_vectors: str, default "error"
|
||||||
@@ -150,7 +176,8 @@ class DBConnection(ABC):
|
|||||||
... for i in range(5):
|
... for i in range(5):
|
||||||
... yield pa.RecordBatch.from_arrays(
|
... yield pa.RecordBatch.from_arrays(
|
||||||
... [
|
... [
|
||||||
... pa.array([[3.1, 4.1], [5.9, 26.5]], pa.list_(pa.float32(), 2)),
|
... pa.array([[3.1, 4.1], [5.9, 26.5]],
|
||||||
|
... pa.list_(pa.float32(), 2)),
|
||||||
... pa.array(["foo", "bar"]),
|
... pa.array(["foo", "bar"]),
|
||||||
... pa.array([10.0, 20.0]),
|
... pa.array([10.0, 20.0]),
|
||||||
... ],
|
... ],
|
||||||
@@ -249,12 +276,15 @@ class LanceDBConnection(DBConnection):
|
|||||||
def uri(self) -> str:
|
def uri(self) -> str:
|
||||||
return self._uri
|
return self._uri
|
||||||
|
|
||||||
def table_names(self) -> list[str]:
|
@override
|
||||||
"""Get the names of all tables in the database.
|
def table_names(
|
||||||
|
self, page_token: Optional[str] = None, limit: int = 10
|
||||||
|
) -> Iterable[str]:
|
||||||
|
"""Get the names of all tables in the database. The names are sorted.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list of str
|
Iterator of str.
|
||||||
A list of table names.
|
A list of table names.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -274,6 +304,7 @@ class LanceDBConnection(DBConnection):
|
|||||||
for file_info in paths
|
for file_info in paths
|
||||||
if file_info.extension == "lance"
|
if file_info.extension == "lance"
|
||||||
]
|
]
|
||||||
|
tables.sort()
|
||||||
return tables
|
return tables
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
@@ -282,6 +313,7 @@ class LanceDBConnection(DBConnection):
|
|||||||
def __contains__(self, name: str) -> bool:
|
def __contains__(self, name: str) -> bool:
|
||||||
return name in self.table_names()
|
return name in self.table_names()
|
||||||
|
|
||||||
|
@override
|
||||||
def create_table(
|
def create_table(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -313,6 +345,7 @@ class LanceDBConnection(DBConnection):
|
|||||||
)
|
)
|
||||||
return tbl
|
return tbl
|
||||||
|
|
||||||
|
@override
|
||||||
def open_table(self, name: str) -> LanceTable:
|
def open_table(self, name: str) -> LanceTable:
|
||||||
"""Open a table in the database.
|
"""Open a table in the database.
|
||||||
|
|
||||||
@@ -327,6 +360,7 @@ class LanceDBConnection(DBConnection):
|
|||||||
"""
|
"""
|
||||||
return LanceTable.open(self, name)
|
return LanceTable.open(self, name)
|
||||||
|
|
||||||
|
@override
|
||||||
def drop_table(self, name: str, ignore_missing: bool = False):
|
def drop_table(self, name: str, ignore_missing: bool = False):
|
||||||
"""Drop a table from the database.
|
"""Drop a table from the database.
|
||||||
|
|
||||||
@@ -345,6 +379,7 @@ class LanceDBConnection(DBConnection):
|
|||||||
if not ignore_missing:
|
if not ignore_missing:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@override
|
||||||
def drop_database(self):
|
def drop_database(self):
|
||||||
filesystem, path = fs_from_uri(self.uri)
|
filesystem, path = fs_from_uri(self.uri)
|
||||||
filesystem.delete_dir(path)
|
filesystem.delete_dir(path)
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
# ruff: noqa: F401
|
||||||
from .base import EmbeddingFunction, EmbeddingFunctionConfig, TextEmbeddingFunction
|
from .base import EmbeddingFunction, EmbeddingFunctionConfig, TextEmbeddingFunction
|
||||||
from .cohere import CohereEmbeddingFunction
|
from .cohere import CohereEmbeddingFunction
|
||||||
|
from .instructor import InstructorEmbeddingFunction
|
||||||
from .open_clip import OpenClipEmbeddings
|
from .open_clip import OpenClipEmbeddings
|
||||||
from .openai import OpenAIEmbeddings
|
from .openai import OpenAIEmbeddings
|
||||||
from .registry import EmbeddingFunctionRegistry, get_registry
|
from .registry import EmbeddingFunctionRegistry, get_registry
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
# Copyright (c) 2023. LanceDB Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
import importlib
|
import importlib
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
@@ -6,7 +18,7 @@ import numpy as np
|
|||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
from pydantic import BaseModel, Field, PrivateAttr
|
from pydantic import BaseModel, Field, PrivateAttr
|
||||||
|
|
||||||
from .utils import TEXT
|
from .utils import TEXT, retry_with_exponential_backoff
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingFunction(BaseModel, ABC):
|
class EmbeddingFunction(BaseModel, ABC):
|
||||||
@@ -21,6 +33,10 @@ class EmbeddingFunction(BaseModel, ABC):
|
|||||||
3. ndims method which returns the number of dimensions of the vector column
|
3. ndims method which returns the number of dimensions of the vector column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("__weakref__",) # pydantic 1.x compatibility
|
||||||
|
max_retries: int = (
|
||||||
|
7 # Setitng 0 disables retires. Maybe this should not be enabled by default,
|
||||||
|
)
|
||||||
_ndims: int = PrivateAttr()
|
_ndims: int = PrivateAttr()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -44,6 +60,25 @@ class EmbeddingFunction(BaseModel, ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def compute_query_embeddings_with_retry(self, *args, **kwargs) -> List[np.array]:
|
||||||
|
"""
|
||||||
|
Compute the embeddings for a given user query with retries
|
||||||
|
"""
|
||||||
|
return retry_with_exponential_backoff(
|
||||||
|
self.compute_query_embeddings, max_retries=self.max_retries
|
||||||
|
)(
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def compute_source_embeddings_with_retry(self, *args, **kwargs) -> List[np.array]:
|
||||||
|
"""
|
||||||
|
Compute the embeddings for the source column in the database with retries
|
||||||
|
"""
|
||||||
|
return retry_with_exponential_backoff(
|
||||||
|
self.compute_source_embeddings, max_retries=self.max_retries
|
||||||
|
)(*args, **kwargs)
|
||||||
|
|
||||||
def sanitize_input(self, texts: TEXT) -> Union[List[str], np.ndarray]:
|
def sanitize_input(self, texts: TEXT) -> Union[List[str], np.ndarray]:
|
||||||
"""
|
"""
|
||||||
Sanitize the input to the embedding function.
|
Sanitize the input to the embedding function.
|
||||||
@@ -103,6 +138,14 @@ class EmbeddingFunction(BaseModel, ABC):
|
|||||||
"""
|
"""
|
||||||
return Field(json_schema_extra={"vector_column_for": self}, **kwargs)
|
return Field(json_schema_extra={"vector_column_for": self}, **kwargs)
|
||||||
|
|
||||||
|
def __eq__(self, __value: object) -> bool:
|
||||||
|
if not hasattr(__value, "__dict__"):
|
||||||
|
return False
|
||||||
|
return vars(self) == vars(__value)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(frozenset(vars(self).items()))
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingFunctionConfig(BaseModel):
|
class EmbeddingFunctionConfig(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class CohereEmbeddingFunction(TextEmbeddingFunction):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
name: str, default "embed-multilingual-v2.0"
|
name: str, default "embed-multilingual-v2.0"
|
||||||
The name of the model to use. See the Cohere documentation for a list of available models.
|
The name of the model to use. See the Cohere documentation for
|
||||||
|
a list of available models.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
@@ -39,7 +40,10 @@ class CohereEmbeddingFunction(TextEmbeddingFunction):
|
|||||||
from lancedb.pydantic import LanceModel, Vector
|
from lancedb.pydantic import LanceModel, Vector
|
||||||
from lancedb.embeddings import EmbeddingFunctionRegistry
|
from lancedb.embeddings import EmbeddingFunctionRegistry
|
||||||
|
|
||||||
cohere = EmbeddingFunctionRegistry.get_instance().get("cohere").create(name="embed-multilingual-v2.0")
|
cohere = EmbeddingFunctionRegistry
|
||||||
|
.get_instance()
|
||||||
|
.get("cohere")
|
||||||
|
.create(name="embed-multilingual-v2.0")
|
||||||
|
|
||||||
class TextModel(LanceModel):
|
class TextModel(LanceModel):
|
||||||
text: str = cohere.SourceField()
|
text: str = cohere.SourceField()
|
||||||
|
|||||||
137
python/lancedb/embeddings/instructor.py
Normal file
137
python/lancedb/embeddings/instructor.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Copyright (c) 2023. LanceDB Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .base import TextEmbeddingFunction
|
||||||
|
from .registry import register
|
||||||
|
from .utils import TEXT, weak_lru
|
||||||
|
|
||||||
|
|
||||||
|
@register("instructor")
|
||||||
|
class InstructorEmbeddingFunction(TextEmbeddingFunction):
|
||||||
|
"""
|
||||||
|
An embedding function that uses the InstructorEmbedding library. Instructor models support multi-task learning, and can be used for a
|
||||||
|
variety of tasks, including text classification, sentence similarity, and document retrieval.
|
||||||
|
If you want to calculate customized embeddings for specific sentences, you may follow the unified template to write instructions:
|
||||||
|
"Represent the `domain` `text_type` for `task_objective`":
|
||||||
|
|
||||||
|
* domain is optional, and it specifies the domain of the text, e.g., science, finance, medicine, etc.
|
||||||
|
* text_type is required, and it specifies the encoding unit, e.g., sentence, document, paragraph, etc.
|
||||||
|
* task_objective is optional, and it specifies the objective of embedding, e.g., retrieve a document, classify the sentence, etc.
|
||||||
|
|
||||||
|
For example, if you want to calculate embeddings for a document, you may write the instruction as follows:
|
||||||
|
"Represent the document for retreival"
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of the model to use. Available models are listed at https://github.com/xlang-ai/instructor-embedding#model-list;
|
||||||
|
The default model is hkunlp/instructor-base
|
||||||
|
batch_size: int, default 32
|
||||||
|
The batch size to use when generating embeddings
|
||||||
|
device: str, default "cpu"
|
||||||
|
The device to use when generating embeddings
|
||||||
|
show_progress_bar: bool, default True
|
||||||
|
Whether to show a progress bar when generating embeddings
|
||||||
|
normalize_embeddings: bool, default True
|
||||||
|
Whether to normalize the embeddings
|
||||||
|
quantize: bool, default False
|
||||||
|
Whether to quantize the model
|
||||||
|
source_instruction: str, default "represent the docuement for retreival"
|
||||||
|
The instruction for the source column
|
||||||
|
query_instruction: str, default "represent the document for retreiving the most similar documents"
|
||||||
|
The instruction for the query
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
import lancedb
|
||||||
|
from lancedb.pydantic import LanceModel, Vector
|
||||||
|
from lancedb.embeddings import get_registry, InstuctorEmbeddingFunction
|
||||||
|
|
||||||
|
instructor = get_registry().get("instructor").create(
|
||||||
|
source_instruction="represent the docuement for retreival",
|
||||||
|
query_instruction="represent the document for retreiving the most similar documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Schema(LanceModel):
|
||||||
|
vector: Vector(instructor.ndims()) = instructor.VectorField()
|
||||||
|
text: str = instructor.SourceField()
|
||||||
|
|
||||||
|
db = lancedb.connect("~/.lancedb")
|
||||||
|
tbl = db.create_table("test", schema=Schema, mode="overwrite")
|
||||||
|
|
||||||
|
texts = [{"text": "Capitalism has been dominant in the Western world since the end of feudalism, but most feel[who?] that..."},
|
||||||
|
{"text": "The disparate impact theory is especially controversial under the Fair Housing Act because the Act..."},
|
||||||
|
{"text": "Disparate impact in United States labor law refers to practices in employment, housing, and other areas that.."}]
|
||||||
|
|
||||||
|
tbl.add(texts)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = "hkunlp/instructor-base"
|
||||||
|
batch_size: int = 32
|
||||||
|
device: str = "cpu"
|
||||||
|
show_progress_bar: bool = True
|
||||||
|
normalize_embeddings: bool = True
|
||||||
|
quantize: bool = False
|
||||||
|
# convert_to_numpy: bool = True # Hardcoding this as numpy can be ingested directly
|
||||||
|
|
||||||
|
source_instruction: str = "represent the document for retrieval"
|
||||||
|
query_instruction: str = (
|
||||||
|
"represent the document for retrieving the most similar documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
@weak_lru(maxsize=1)
|
||||||
|
def ndims(self):
|
||||||
|
model = self.get_model()
|
||||||
|
return model.encode("foo").shape[0]
|
||||||
|
|
||||||
|
def compute_query_embeddings(self, query: str, *args, **kwargs) -> List[np.array]:
|
||||||
|
return self.generate_embeddings([[self.query_instruction, query]])
|
||||||
|
|
||||||
|
def compute_source_embeddings(self, texts: TEXT, *args, **kwargs) -> List[np.array]:
|
||||||
|
texts = self.sanitize_input(texts)
|
||||||
|
texts_formatted = []
|
||||||
|
for text in texts:
|
||||||
|
texts_formatted.append([self.source_instruction, text])
|
||||||
|
return self.generate_embeddings(texts_formatted)
|
||||||
|
|
||||||
|
def generate_embeddings(self, texts: List) -> List:
|
||||||
|
model = self.get_model()
|
||||||
|
res = model.encode(
|
||||||
|
texts,
|
||||||
|
batch_size=self.batch_size,
|
||||||
|
show_progress_bar=self.show_progress_bar,
|
||||||
|
normalize_embeddings=self.normalize_embeddings,
|
||||||
|
).tolist()
|
||||||
|
return res
|
||||||
|
|
||||||
|
@weak_lru(maxsize=1)
|
||||||
|
def get_model(self):
|
||||||
|
instructor_embedding = self.safe_import(
|
||||||
|
"InstructorEmbedding", "InstructorEmbedding"
|
||||||
|
)
|
||||||
|
torch = self.safe_import("torch", "torch")
|
||||||
|
|
||||||
|
model = instructor_embedding.INSTRUCTOR(self.name)
|
||||||
|
if self.quantize:
|
||||||
|
if (
|
||||||
|
"qnnpack" in torch.backends.quantized.supported_engines
|
||||||
|
): # fix for https://github.com/pytorch/pytorch/issues/29327
|
||||||
|
torch.backends.quantized.engine = "qnnpack"
|
||||||
|
model = torch.quantization.quantize_dynamic(
|
||||||
|
model, {torch.nn.Linear}, dtype=torch.qint8
|
||||||
|
)
|
||||||
|
return model
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
# Copyright (c) 2023. LanceDB Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
# Copyright (c) 2023. LanceDB Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
# Copyright (c) 2023. LanceDB Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -5,6 +17,7 @@ from cachetools import cached
|
|||||||
|
|
||||||
from .base import TextEmbeddingFunction
|
from .base import TextEmbeddingFunction
|
||||||
from .registry import register
|
from .registry import register
|
||||||
|
from .utils import weak_lru
|
||||||
|
|
||||||
|
|
||||||
@register("sentence-transformers")
|
@register("sentence-transformers")
|
||||||
@@ -30,7 +43,7 @@ class SentenceTransformerEmbeddings(TextEmbeddingFunction):
|
|||||||
name and device. This is cached so that the model is only loaded
|
name and device. This is cached so that the model is only loaded
|
||||||
once per process.
|
once per process.
|
||||||
"""
|
"""
|
||||||
return self.__class__.get_embedding_model(self.name, self.device)
|
return self.get_embedding_model()
|
||||||
|
|
||||||
def ndims(self):
|
def ndims(self):
|
||||||
if self._ndims is None:
|
if self._ndims is None:
|
||||||
@@ -54,9 +67,8 @@ class SentenceTransformerEmbeddings(TextEmbeddingFunction):
|
|||||||
normalize_embeddings=self.normalize,
|
normalize_embeddings=self.normalize,
|
||||||
).tolist()
|
).tolist()
|
||||||
|
|
||||||
@classmethod
|
@weak_lru(maxsize=1)
|
||||||
@cached(cache={})
|
def get_embedding_model(self):
|
||||||
def get_embedding_model(cls, name, device):
|
|
||||||
"""
|
"""
|
||||||
Get the sentence-transformers embedding model specified by the
|
Get the sentence-transformers embedding model specified by the
|
||||||
name and device. This is cached so that the model is only loaded
|
name and device. This is cached so that the model is only loaded
|
||||||
@@ -71,7 +83,7 @@ class SentenceTransformerEmbeddings(TextEmbeddingFunction):
|
|||||||
|
|
||||||
TODO: use lru_cache instead with a reasonable/configurable maxsize
|
TODO: use lru_cache instead with a reasonable/configurable maxsize
|
||||||
"""
|
"""
|
||||||
sentence_transformers = cls.safe_import(
|
sentence_transformers = self.safe_import(
|
||||||
"sentence_transformers", "sentence-transformers"
|
"sentence_transformers", "sentence-transformers"
|
||||||
)
|
)
|
||||||
return sentence_transformers.SentenceTransformer(name, device=device)
|
return sentence_transformers.SentenceTransformer(self.name, device=self.device)
|
||||||
|
|||||||
@@ -11,10 +11,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import functools
|
||||||
import math
|
import math
|
||||||
|
import random
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import weakref
|
||||||
from typing import Callable, List, Union
|
from typing import Callable, List, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -162,6 +166,99 @@ class FunctionWrapper:
|
|||||||
yield from _chunker(arr)
|
yield from _chunker(arr)
|
||||||
|
|
||||||
|
|
||||||
|
def weak_lru(maxsize=128):
|
||||||
|
"""
|
||||||
|
LRU cache that keeps weak references to the objects it caches. Only caches the latest instance of the objects to make sure memory usage
|
||||||
|
is bounded.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
maxsize : int, default 128
|
||||||
|
The maximum number of objects to cache.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Callable
|
||||||
|
A decorator that can be applied to a method.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> class Foo:
|
||||||
|
... @weak_lru()
|
||||||
|
... def bar(self, x):
|
||||||
|
... return x
|
||||||
|
>>> foo = Foo()
|
||||||
|
>>> foo.bar(1)
|
||||||
|
1
|
||||||
|
>>> foo.bar(2)
|
||||||
|
2
|
||||||
|
>>> foo.bar(1)
|
||||||
|
1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
@functools.lru_cache(maxsize)
|
||||||
|
def _func(_self, *args, **kwargs):
|
||||||
|
return func(_self(), *args, **kwargs)
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def inner(self, *args, **kwargs):
|
||||||
|
return _func(weakref.ref(self), *args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def retry_with_exponential_backoff(
|
||||||
|
func,
|
||||||
|
initial_delay: float = 1,
|
||||||
|
exponential_base: float = 2,
|
||||||
|
jitter: bool = True,
|
||||||
|
max_retries: int = 7,
|
||||||
|
# errors: tuple = (),
|
||||||
|
):
|
||||||
|
"""Retry a function with exponential backoff.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (function): The function to be retried.
|
||||||
|
initial_delay (float): Initial delay in seconds (default is 1).
|
||||||
|
exponential_base (float): The base for exponential backoff (default is 2).
|
||||||
|
jitter (bool): Whether to add jitter to the delay (default is True).
|
||||||
|
max_retries (int): Maximum number of retries (default is 10).
|
||||||
|
errors (tuple): Tuple of specific exceptions to retry on (default is (openai.error.RateLimitError,)).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
function: The decorated function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
num_retries = 0
|
||||||
|
delay = initial_delay
|
||||||
|
|
||||||
|
# Loop until a successful response or max_retries is hit or an exception is raised
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
# Currently retrying on all exceptions as there is no way to know the format of the error msgs used by different APIs
|
||||||
|
# We'll log the error and say that it is assumed that if this portion errors out, it's due to rate limit but the user
|
||||||
|
# should check the error message to be sure
|
||||||
|
except Exception as e:
|
||||||
|
num_retries += 1
|
||||||
|
|
||||||
|
if num_retries > max_retries:
|
||||||
|
raise Exception(
|
||||||
|
f"Maximum number of retries ({max_retries}) exceeded."
|
||||||
|
)
|
||||||
|
|
||||||
|
delay *= exponential_base * (1 + jitter * random.random())
|
||||||
|
LOGGER.info(f"Retrying in {delay:.2f} seconds due to {e}")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def url_retrieve(url: str):
|
def url_retrieve(url: str):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Literal, Optional, Type, Union
|
from typing import TYPE_CHECKING, List, Literal, Optional, Type, Union
|
||||||
|
|
||||||
import deprecation
|
import deprecation
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -23,14 +23,49 @@ import pydantic
|
|||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .common import VECTOR_COLUMN_NAME
|
from .common import VECTOR_COLUMN_NAME
|
||||||
from .pydantic import LanceModel
|
|
||||||
from .util import safe_import_pandas
|
from .util import safe_import_pandas
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .pydantic import LanceModel
|
||||||
|
|
||||||
pd = safe_import_pandas()
|
pd = safe_import_pandas()
|
||||||
|
|
||||||
|
|
||||||
class Query(pydantic.BaseModel):
|
class Query(pydantic.BaseModel):
|
||||||
"""A Query"""
|
"""The LanceDB Query
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
vector : List[float]
|
||||||
|
the vector to search for
|
||||||
|
filter : Optional[str]
|
||||||
|
sql filter to refine the query with, optional
|
||||||
|
prefilter : bool
|
||||||
|
if True then apply the filter before vector search
|
||||||
|
k : int
|
||||||
|
top k results to return
|
||||||
|
metric : str
|
||||||
|
the distance metric between a pair of vectors,
|
||||||
|
|
||||||
|
can support L2 (default), Cosine and Dot.
|
||||||
|
[metric definitions][search]
|
||||||
|
columns : Optional[List[str]]
|
||||||
|
which columns to return in the results
|
||||||
|
nprobes : int
|
||||||
|
The number of probes used - optional
|
||||||
|
|
||||||
|
- A higher number makes search more accurate but also slower.
|
||||||
|
|
||||||
|
- See discussion in [Querying an ANN Index][querying-an-ann-index] for
|
||||||
|
tuning advice.
|
||||||
|
refine_factor : Optional[int]
|
||||||
|
Refine the results by reading extra elements and re-ranking them in memory - optional
|
||||||
|
|
||||||
|
- A higher number makes search more accurate but also slower.
|
||||||
|
|
||||||
|
- See discussion in [Querying an ANN Index][querying-an-ann-index] for
|
||||||
|
tuning advice.
|
||||||
|
"""
|
||||||
|
|
||||||
vector_column: str = VECTOR_COLUMN_NAME
|
vector_column: str = VECTOR_COLUMN_NAME
|
||||||
|
|
||||||
@@ -61,6 +96,10 @@ class Query(pydantic.BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class LanceQueryBuilder(ABC):
|
class LanceQueryBuilder(ABC):
|
||||||
|
"""Build LanceDB query based on specific query type:
|
||||||
|
vector or full text search.
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
cls,
|
cls,
|
||||||
@@ -103,7 +142,7 @@ class LanceQueryBuilder(ABC):
|
|||||||
if not isinstance(query, (list, np.ndarray)):
|
if not isinstance(query, (list, np.ndarray)):
|
||||||
conf = table.embedding_functions.get(vector_column_name)
|
conf = table.embedding_functions.get(vector_column_name)
|
||||||
if conf is not None:
|
if conf is not None:
|
||||||
query = conf.function.compute_query_embeddings(query)[0]
|
query = conf.function.compute_query_embeddings_with_retry(query)[0]
|
||||||
else:
|
else:
|
||||||
msg = f"No embedding function for {vector_column_name}"
|
msg = f"No embedding function for {vector_column_name}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
@@ -114,7 +153,7 @@ class LanceQueryBuilder(ABC):
|
|||||||
else:
|
else:
|
||||||
conf = table.embedding_functions.get(vector_column_name)
|
conf = table.embedding_functions.get(vector_column_name)
|
||||||
if conf is not None:
|
if conf is not None:
|
||||||
query = conf.function.compute_query_embeddings(query)[0]
|
query = conf.function.compute_query_embeddings_with_retry(query)[0]
|
||||||
return query, "vector"
|
return query, "vector"
|
||||||
else:
|
else:
|
||||||
return query, "fts"
|
return query, "fts"
|
||||||
@@ -133,11 +172,11 @@ class LanceQueryBuilder(ABC):
|
|||||||
deprecated_in="0.3.1",
|
deprecated_in="0.3.1",
|
||||||
removed_in="0.4.0",
|
removed_in="0.4.0",
|
||||||
current_version=__version__,
|
current_version=__version__,
|
||||||
details="Use the bar function instead",
|
details="Use to_pandas() instead",
|
||||||
)
|
)
|
||||||
def to_df(self) -> "pd.DataFrame":
|
def to_df(self) -> "pd.DataFrame":
|
||||||
"""
|
"""
|
||||||
Deprecated alias for `to_pandas()`. Please use `to_pandas()` instead.
|
*Deprecated alias for `to_pandas()`. Please use `to_pandas()` instead.*
|
||||||
|
|
||||||
Execute the query and return the results as a pandas DataFrame.
|
Execute the query and return the results as a pandas DataFrame.
|
||||||
In addition to the selected columns, LanceDB also returns a vector
|
In addition to the selected columns, LanceDB also returns a vector
|
||||||
@@ -226,13 +265,20 @@ class LanceQueryBuilder(ABC):
|
|||||||
self._columns = columns
|
self._columns = columns
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def where(self, where) -> LanceQueryBuilder:
|
def where(self, where: str, prefilter: bool = False) -> LanceQueryBuilder:
|
||||||
"""Set the where clause.
|
"""Set the where clause.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
where: str
|
where: str
|
||||||
The where clause.
|
The where clause which is a valid SQL where clause. See
|
||||||
|
`Lance filter pushdown <https://lancedb.github.io/lance/read_and_write.html#filter-push-down>`_
|
||||||
|
for valid SQL expressions.
|
||||||
|
prefilter: bool, default False
|
||||||
|
If True, apply the filter before vector search, otherwise the
|
||||||
|
filter is applied on the result of vector search.
|
||||||
|
This feature is **EXPERIMENTAL** and may be removed and modified
|
||||||
|
without warning in the future.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -240,13 +286,12 @@ class LanceQueryBuilder(ABC):
|
|||||||
The LanceQueryBuilder object.
|
The LanceQueryBuilder object.
|
||||||
"""
|
"""
|
||||||
self._where = where
|
self._where = where
|
||||||
|
self._prefilter = prefilter
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class LanceVectorQueryBuilder(LanceQueryBuilder):
|
class LanceVectorQueryBuilder(LanceQueryBuilder):
|
||||||
"""
|
"""
|
||||||
A builder for nearest neighbor queries for LanceDB.
|
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> import lancedb
|
>>> import lancedb
|
||||||
@@ -302,7 +347,7 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
|
|||||||
Higher values will yield better recall (more likely to find vectors if
|
Higher values will yield better recall (more likely to find vectors if
|
||||||
they exist) at the expense of latency.
|
they exist) at the expense of latency.
|
||||||
|
|
||||||
See discussion in [Querying an ANN Index][../querying-an-ann-index] for
|
See discussion in [Querying an ANN Index][querying-an-ann-index] for
|
||||||
tuning advice.
|
tuning advice.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -369,14 +414,14 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
where: str
|
where: str
|
||||||
The where clause.
|
The where clause which is a valid SQL where clause. See
|
||||||
|
`Lance filter pushdown <https://lancedb.github.io/lance/read_and_write.html#filter-push-down>`_
|
||||||
|
for valid SQL expressions.
|
||||||
prefilter: bool, default False
|
prefilter: bool, default False
|
||||||
If True, apply the filter before vector search, otherwise the
|
If True, apply the filter before vector search, otherwise the
|
||||||
filter is applied on the result of vector search.
|
filter is applied on the result of vector search.
|
||||||
This feature is **EXPERIMENTAL** and may be removed and modified
|
This feature is **EXPERIMENTAL** and may be removed and modified
|
||||||
without warning in the future. Currently this is only supported
|
without warning in the future.
|
||||||
in OSS and can only be used with a table that does not have an ANN
|
|
||||||
index.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -389,6 +434,8 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
|
|||||||
|
|
||||||
|
|
||||||
class LanceFtsQueryBuilder(LanceQueryBuilder):
|
class LanceFtsQueryBuilder(LanceQueryBuilder):
|
||||||
|
"""A builder for full text search for LanceDB."""
|
||||||
|
|
||||||
def __init__(self, table: "lancedb.table.Table", query: str):
|
def __init__(self, table: "lancedb.table.Table", query: str):
|
||||||
super().__init__(table)
|
super().__init__(table)
|
||||||
self._query = query
|
self._query = query
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
from typing import Any, Callable, Dict, Optional, Union
|
from typing import Any, Callable, Dict, Iterable, Optional, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import attrs
|
import attrs
|
||||||
@@ -151,15 +151,14 @@ class RestfulLanceDBClient:
|
|||||||
return await deserialize(resp)
|
return await deserialize(resp)
|
||||||
|
|
||||||
@_check_not_closed
|
@_check_not_closed
|
||||||
async def list_tables(self, limit: int, page_token: str):
|
async def list_tables(
|
||||||
|
self, limit: int, page_token: Optional[str] = None
|
||||||
|
) -> Iterable[str]:
|
||||||
"""List all tables in the database."""
|
"""List all tables in the database."""
|
||||||
try:
|
if page_token is None:
|
||||||
json = await self.get(
|
page_token = ""
|
||||||
"/v1/table/", {"limit": limit, "page_token": page_token}
|
json = await self.get("/v1/table/", {"limit": limit, "page_token": page_token})
|
||||||
)
|
return json["tables"]
|
||||||
return json["tables"]
|
|
||||||
except StopAsyncIteration:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@_check_not_closed
|
@_check_not_closed
|
||||||
async def query(self, table_name: str, query: VectorQuery) -> VectorQueryResult:
|
async def query(self, table_name: str, query: VectorQuery) -> VectorQueryResult:
|
||||||
|
|||||||
@@ -12,17 +12,23 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Iterator, Optional
|
from typing import Iterable, List, Optional, Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
|
from overrides import override
|
||||||
|
|
||||||
from ..common import DATA
|
from ..common import DATA
|
||||||
from ..db import DBConnection
|
from ..db import DBConnection
|
||||||
|
from ..embeddings import EmbeddingFunctionConfig
|
||||||
|
from ..pydantic import LanceModel
|
||||||
from ..table import Table, _sanitize_data
|
from ..table import Table, _sanitize_data
|
||||||
from .arrow import to_ipc_binary
|
from .arrow import to_ipc_binary
|
||||||
from .client import ARROW_STREAM_CONTENT_TYPE, RestfulLanceDBClient
|
from .client import ARROW_STREAM_CONTENT_TYPE, RestfulLanceDBClient
|
||||||
|
from .errors import LanceDBClientError
|
||||||
|
|
||||||
|
|
||||||
class RemoteDBConnection(DBConnection):
|
class RemoteDBConnection(DBConnection):
|
||||||
@@ -50,14 +56,20 @@ class RemoteDBConnection(DBConnection):
|
|||||||
self._loop = asyncio.get_event_loop()
|
self._loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"RemoveConnect(name={self.db_name})"
|
return f"RemoteConnect(name={self.db_name})"
|
||||||
|
|
||||||
def table_names(self, last_token: str, limit=10) -> Iterator[str]:
|
@override
|
||||||
|
def table_names(
|
||||||
|
self, page_token: Optional[str] = None, limit: int = 10
|
||||||
|
) -> Iterable[str]:
|
||||||
"""List the names of all tables in the database.
|
"""List the names of all tables in the database.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
last_token: str
|
page_token: str
|
||||||
The last token to start the new page.
|
The last token to start the new page.
|
||||||
|
limit: int, default 10
|
||||||
|
The maximum number of tables to return for each page.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -65,15 +77,16 @@ class RemoteDBConnection(DBConnection):
|
|||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
result = self._loop.run_until_complete(
|
result = self._loop.run_until_complete(
|
||||||
self._client.list_tables(limit, last_token)
|
self._client.list_tables(limit, page_token)
|
||||||
)
|
)
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
last_token = result[len(result) - 1]
|
page_token = result[len(result) - 1]
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
for item in result:
|
for item in result:
|
||||||
yield result
|
yield item
|
||||||
|
|
||||||
|
@override
|
||||||
def open_table(self, name: str) -> Table:
|
def open_table(self, name: str) -> Table:
|
||||||
"""Open a Lance Table in the database.
|
"""Open a Lance Table in the database.
|
||||||
|
|
||||||
@@ -88,23 +101,142 @@ class RemoteDBConnection(DBConnection):
|
|||||||
"""
|
"""
|
||||||
from .table import RemoteTable
|
from .table import RemoteTable
|
||||||
|
|
||||||
# TODO: check if table exists
|
# check if table exists
|
||||||
|
try:
|
||||||
|
self._loop.run_until_complete(
|
||||||
|
self._client.post(f"/v1/table/{name}/describe/")
|
||||||
|
)
|
||||||
|
except LanceDBClientError as err:
|
||||||
|
if str(err).startswith("Not found"):
|
||||||
|
logging.error(
|
||||||
|
f"Table {name} does not exist. "
|
||||||
|
f"Please first call db.create_table({name}, data)"
|
||||||
|
)
|
||||||
return RemoteTable(self, name)
|
return RemoteTable(self, name)
|
||||||
|
|
||||||
|
@override
|
||||||
def create_table(
|
def create_table(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
data: DATA = None,
|
data: DATA = None,
|
||||||
schema: pa.Schema = None,
|
schema: Optional[Union[pa.Schema, LanceModel]] = None,
|
||||||
on_bad_vectors: str = "error",
|
on_bad_vectors: str = "error",
|
||||||
fill_value: float = 0.0,
|
fill_value: float = 0.0,
|
||||||
|
embedding_functions: Optional[List[EmbeddingFunctionConfig]] = None,
|
||||||
) -> Table:
|
) -> Table:
|
||||||
|
"""Create a [Table][lancedb.table.Table] in the database.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name: str
|
||||||
|
The name of the table.
|
||||||
|
data: The data to initialize the table, *optional*
|
||||||
|
User must provide at least one of `data` or `schema`.
|
||||||
|
Acceptable types are:
|
||||||
|
|
||||||
|
- dict or list-of-dict
|
||||||
|
|
||||||
|
- pandas.DataFrame
|
||||||
|
|
||||||
|
- pyarrow.Table or pyarrow.RecordBatch
|
||||||
|
schema: The schema of the table, *optional*
|
||||||
|
Acceptable types are:
|
||||||
|
|
||||||
|
- pyarrow.Schema
|
||||||
|
|
||||||
|
- [LanceModel][lancedb.pydantic.LanceModel]
|
||||||
|
on_bad_vectors: str, default "error"
|
||||||
|
What to do if any of the vectors are not the same size or contains NaNs.
|
||||||
|
One of "error", "drop", "fill".
|
||||||
|
fill_value: float
|
||||||
|
The value to use when filling vectors. Only used if on_bad_vectors="fill".
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
LanceTable
|
||||||
|
A reference to the newly created table.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
The vector index won't be created by default.
|
||||||
|
To create the index, call the `create_index` method on the table.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
Can create with list of tuples or dictionaries:
|
||||||
|
|
||||||
|
>>> import lancedb
|
||||||
|
>>> db = lancedb.connect("db://...", api_key="...", region="...") # doctest: +SKIP
|
||||||
|
>>> data = [{"vector": [1.1, 1.2], "lat": 45.5, "long": -122.7},
|
||||||
|
... {"vector": [0.2, 1.8], "lat": 40.1, "long": -74.1}]
|
||||||
|
>>> db.create_table("my_table", data) # doctest: +SKIP
|
||||||
|
LanceTable(my_table)
|
||||||
|
|
||||||
|
You can also pass a pandas DataFrame:
|
||||||
|
|
||||||
|
>>> import pandas as pd
|
||||||
|
>>> data = pd.DataFrame({
|
||||||
|
... "vector": [[1.1, 1.2], [0.2, 1.8]],
|
||||||
|
... "lat": [45.5, 40.1],
|
||||||
|
... "long": [-122.7, -74.1]
|
||||||
|
... })
|
||||||
|
>>> db.create_table("table2", data) # doctest: +SKIP
|
||||||
|
LanceTable(table2)
|
||||||
|
|
||||||
|
>>> custom_schema = pa.schema([
|
||||||
|
... pa.field("vector", pa.list_(pa.float32(), 2)),
|
||||||
|
... pa.field("lat", pa.float32()),
|
||||||
|
... pa.field("long", pa.float32())
|
||||||
|
... ])
|
||||||
|
>>> db.create_table("table3", data, schema = custom_schema) # doctest: +SKIP
|
||||||
|
LanceTable(table3)
|
||||||
|
|
||||||
|
It is also possible to create an table from `[Iterable[pa.RecordBatch]]`:
|
||||||
|
|
||||||
|
>>> import pyarrow as pa
|
||||||
|
>>> def make_batches():
|
||||||
|
... for i in range(5):
|
||||||
|
... yield pa.RecordBatch.from_arrays(
|
||||||
|
... [
|
||||||
|
... pa.array([[3.1, 4.1], [5.9, 26.5]],
|
||||||
|
... pa.list_(pa.float32(), 2)),
|
||||||
|
... pa.array(["foo", "bar"]),
|
||||||
|
... pa.array([10.0, 20.0]),
|
||||||
|
... ],
|
||||||
|
... ["vector", "item", "price"],
|
||||||
|
... )
|
||||||
|
>>> schema=pa.schema([
|
||||||
|
... pa.field("vector", pa.list_(pa.float32(), 2)),
|
||||||
|
... pa.field("item", pa.utf8()),
|
||||||
|
... pa.field("price", pa.float32()),
|
||||||
|
... ])
|
||||||
|
>>> db.create_table("table4", make_batches(), schema=schema) # doctest: +SKIP
|
||||||
|
LanceTable(table4)
|
||||||
|
|
||||||
|
"""
|
||||||
if data is None and schema is None:
|
if data is None and schema is None:
|
||||||
raise ValueError("Either data or schema must be provided.")
|
raise ValueError("Either data or schema must be provided.")
|
||||||
|
if embedding_functions is not None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"embedding_functions is not supported for remote databases."
|
||||||
|
"Please vote https://github.com/lancedb/lancedb/issues/626 "
|
||||||
|
"for this feature."
|
||||||
|
)
|
||||||
|
|
||||||
|
if inspect.isclass(schema) and issubclass(schema, LanceModel):
|
||||||
|
# convert LanceModel to pyarrow schema
|
||||||
|
# note that it's possible this contains
|
||||||
|
# embedding function metadata already
|
||||||
|
schema = schema.to_arrow_schema()
|
||||||
|
|
||||||
if data is not None:
|
if data is not None:
|
||||||
data = _sanitize_data(
|
data = _sanitize_data(
|
||||||
data, schema, on_bad_vectors=on_bad_vectors, fill_value=fill_value
|
data,
|
||||||
|
schema,
|
||||||
|
metadata=None,
|
||||||
|
on_bad_vectors=on_bad_vectors,
|
||||||
|
fill_value=fill_value,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if schema is None:
|
if schema is None:
|
||||||
@@ -126,6 +258,7 @@ class RemoteDBConnection(DBConnection):
|
|||||||
)
|
)
|
||||||
return RemoteTable(self, name)
|
return RemoteTable(self, name)
|
||||||
|
|
||||||
|
@override
|
||||||
def drop_table(self, name: str):
|
def drop_table(self, name: str):
|
||||||
"""Drop a table from the database.
|
"""Drop a table from the database.
|
||||||
|
|
||||||
|
|||||||
@@ -37,34 +37,81 @@ class RemoteTable(Table):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def schema(self) -> pa.Schema:
|
def schema(self) -> pa.Schema:
|
||||||
"""Return the schema of the table."""
|
"""The [Arrow Schema](https://arrow.apache.org/docs/python/api/datatypes.html#)
|
||||||
|
of this Table
|
||||||
|
|
||||||
|
"""
|
||||||
resp = self._conn._loop.run_until_complete(
|
resp = self._conn._loop.run_until_complete(
|
||||||
self._conn._client.post(f"/v1/table/{self._name}/describe/")
|
self._conn._client.post(f"/v1/table/{self._name}/describe/")
|
||||||
)
|
)
|
||||||
schema = json_to_schema(resp["schema"])
|
schema = json_to_schema(resp["schema"])
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self) -> int:
|
||||||
|
"""Get the current version of the table"""
|
||||||
|
resp = self._conn._loop.run_until_complete(
|
||||||
|
self._conn._client.post(f"/v1/table/{self._name}/describe/")
|
||||||
|
)
|
||||||
|
return resp["version"]
|
||||||
|
|
||||||
def to_arrow(self) -> pa.Table:
|
def to_arrow(self) -> pa.Table:
|
||||||
"""Return the table as an Arrow table."""
|
"""to_arrow() is not supported on the LanceDB cloud"""
|
||||||
raise NotImplementedError("to_arrow() is not supported on the LanceDB cloud")
|
raise NotImplementedError("to_arrow() is not supported on the LanceDB cloud")
|
||||||
|
|
||||||
def to_pandas(self):
|
def to_pandas(self):
|
||||||
"""Return the table as a Pandas DataFrame.
|
"""to_pandas() is not supported on the LanceDB cloud"""
|
||||||
|
|
||||||
Intercept `to_arrow()` for better error message.
|
|
||||||
"""
|
|
||||||
return NotImplementedError("to_pandas() is not supported on the LanceDB cloud")
|
return NotImplementedError("to_pandas() is not supported on the LanceDB cloud")
|
||||||
|
|
||||||
def create_index(
|
def create_index(
|
||||||
self,
|
self,
|
||||||
metric="L2",
|
metric="L2",
|
||||||
num_partitions=256,
|
|
||||||
num_sub_vectors=96,
|
|
||||||
vector_column_name: str = VECTOR_COLUMN_NAME,
|
vector_column_name: str = VECTOR_COLUMN_NAME,
|
||||||
replace: bool = True,
|
index_cache_size: Optional[int] = None,
|
||||||
accelerator: Optional[str] = None,
|
|
||||||
):
|
):
|
||||||
raise NotImplementedError
|
"""Create an index on the table.
|
||||||
|
Currently, the only parameters that matter are
|
||||||
|
the metric and the vector column name.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
metric : str
|
||||||
|
The metric to use for the index. Default is "L2".
|
||||||
|
vector_column_name : str
|
||||||
|
The name of the vector column. Default is "vector".
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> import lancedb
|
||||||
|
>>> import uuid
|
||||||
|
>>> from lancedb.schema import vector
|
||||||
|
>>> db = lancedb.connect("db://...", api_key="...", region="...") # doctest: +SKIP
|
||||||
|
>>> table_name = uuid.uuid4().hex
|
||||||
|
>>> schema = pa.schema(
|
||||||
|
... [
|
||||||
|
... pa.field("id", pa.uint32(), False),
|
||||||
|
... pa.field("vector", vector(128), False),
|
||||||
|
... pa.field("s", pa.string(), False),
|
||||||
|
... ]
|
||||||
|
... )
|
||||||
|
>>> table = db.create_table( # doctest: +SKIP
|
||||||
|
... table_name, # doctest: +SKIP
|
||||||
|
... schema=schema, # doctest: +SKIP
|
||||||
|
... )
|
||||||
|
>>> table.create_index("L2", "vector") # doctest: +SKIP
|
||||||
|
"""
|
||||||
|
index_type = "vector"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"column": vector_column_name,
|
||||||
|
"index_type": index_type,
|
||||||
|
"metric_type": metric,
|
||||||
|
"index_cache_size": index_cache_size,
|
||||||
|
}
|
||||||
|
resp = self._conn._loop.run_until_complete(
|
||||||
|
self._conn._client.post(f"/v1/table/{self._name}/create_index/", data=data)
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
self,
|
self,
|
||||||
@@ -73,6 +120,28 @@ class RemoteTable(Table):
|
|||||||
on_bad_vectors: str = "error",
|
on_bad_vectors: str = "error",
|
||||||
fill_value: float = 0.0,
|
fill_value: float = 0.0,
|
||||||
) -> int:
|
) -> int:
|
||||||
|
"""Add more data to the [Table](Table). It has the same API signature as the OSS version.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: DATA
|
||||||
|
The data to insert into the table. Acceptable types are:
|
||||||
|
|
||||||
|
- dict or list-of-dict
|
||||||
|
|
||||||
|
- pandas.DataFrame
|
||||||
|
|
||||||
|
- pyarrow.Table or pyarrow.RecordBatch
|
||||||
|
mode: str
|
||||||
|
The mode to use when writing the data. Valid values are
|
||||||
|
"append" and "overwrite".
|
||||||
|
on_bad_vectors: str, default "error"
|
||||||
|
What to do if any of the vectors are not the same size or contains NaNs.
|
||||||
|
One of "error", "drop", "fill".
|
||||||
|
fill_value: float, default 0.
|
||||||
|
The value to use when filling vectors. Only used if on_bad_vectors="fill".
|
||||||
|
|
||||||
|
"""
|
||||||
data = _sanitize_data(
|
data = _sanitize_data(
|
||||||
data,
|
data,
|
||||||
self.schema,
|
self.schema,
|
||||||
@@ -96,16 +165,110 @@ class RemoteTable(Table):
|
|||||||
def search(
|
def search(
|
||||||
self, query: Union[VEC, str], vector_column_name: str = VECTOR_COLUMN_NAME
|
self, query: Union[VEC, str], vector_column_name: str = VECTOR_COLUMN_NAME
|
||||||
) -> LanceVectorQueryBuilder:
|
) -> LanceVectorQueryBuilder:
|
||||||
|
"""Create a search query to find the nearest neighbors
|
||||||
|
of the given query vector. We currently support [vector search][search]
|
||||||
|
|
||||||
|
All query options are defined in [Query][lancedb.query.Query].
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> import lancedb
|
||||||
|
>>> db = lancedb.connect("db://...", api_key="...", region="...") # doctest: +SKIP
|
||||||
|
>>> data = [
|
||||||
|
... {"original_width": 100, "caption": "bar", "vector": [0.1, 2.3, 4.5]},
|
||||||
|
... {"original_width": 2000, "caption": "foo", "vector": [0.5, 3.4, 1.3]},
|
||||||
|
... {"original_width": 3000, "caption": "test", "vector": [0.3, 6.2, 2.6]}
|
||||||
|
... ]
|
||||||
|
>>> table = db.create_table("my_table", data) # doctest: +SKIP
|
||||||
|
>>> query = [0.4, 1.4, 2.4]
|
||||||
|
>>> (table.search(query, vector_column_name="vector") # doctest: +SKIP
|
||||||
|
... .where("original_width > 1000", prefilter=True) # doctest: +SKIP
|
||||||
|
... .select(["caption", "original_width"]) # doctest: +SKIP
|
||||||
|
... .limit(2) # doctest: +SKIP
|
||||||
|
... .to_pandas()) # doctest: +SKIP
|
||||||
|
caption original_width vector _distance # doctest: +SKIP
|
||||||
|
0 foo 2000 [0.5, 3.4, 1.3] 5.220000 # doctest: +SKIP
|
||||||
|
1 test 3000 [0.3, 6.2, 2.6] 23.089996 # doctest: +SKIP
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
|
The targetted vector to search for.
|
||||||
|
|
||||||
|
- *default None*.
|
||||||
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
|
|
||||||
|
- If None then the select/where/limit clauses are applied to filter
|
||||||
|
the table
|
||||||
|
vector_column_name: str
|
||||||
|
The name of the vector column to search.
|
||||||
|
*default "vector"*
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
LanceQueryBuilder
|
||||||
|
A query builder object representing the query.
|
||||||
|
Once executed, the query returns
|
||||||
|
|
||||||
|
- selected columns
|
||||||
|
|
||||||
|
- the vector
|
||||||
|
|
||||||
|
- and also the "_distance" column which is the distance between the query
|
||||||
|
vector and the returned vector.
|
||||||
|
"""
|
||||||
return LanceVectorQueryBuilder(self, query, vector_column_name)
|
return LanceVectorQueryBuilder(self, query, vector_column_name)
|
||||||
|
|
||||||
def _execute_query(self, query: Query) -> pa.Table:
|
def _execute_query(self, query: Query) -> pa.Table:
|
||||||
if query.prefilter:
|
|
||||||
raise NotImplementedError("Cloud support for prefiltering is coming soon")
|
|
||||||
result = self._conn._client.query(self._name, query)
|
result = self._conn._client.query(self._name, query)
|
||||||
return self._conn._loop.run_until_complete(result).to_arrow()
|
return self._conn._loop.run_until_complete(result).to_arrow()
|
||||||
|
|
||||||
def delete(self, predicate: str):
|
def delete(self, predicate: str):
|
||||||
"""Delete rows from the table."""
|
"""Delete rows from the table.
|
||||||
|
|
||||||
|
This can be used to delete a single row, many rows, all rows, or
|
||||||
|
sometimes no rows (if your predicate matches nothing).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
predicate: str
|
||||||
|
The SQL where clause to use when deleting rows.
|
||||||
|
|
||||||
|
- For example, 'x = 2' or 'x IN (1, 2, 3)'.
|
||||||
|
|
||||||
|
The filter must not be empty, or it will error.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> import lancedb
|
||||||
|
>>> data = [
|
||||||
|
... {"x": 1, "vector": [1, 2]},
|
||||||
|
... {"x": 2, "vector": [3, 4]},
|
||||||
|
... {"x": 3, "vector": [5, 6]}
|
||||||
|
... ]
|
||||||
|
>>> db = lancedb.connect("db://...", api_key="...", region="...") # doctest: +SKIP
|
||||||
|
>>> table = db.create_table("my_table", data) # doctest: +SKIP
|
||||||
|
>>> table.search([10,10]).to_pandas() # doctest: +SKIP
|
||||||
|
x vector _distance # doctest: +SKIP
|
||||||
|
0 3 [5.0, 6.0] 41.0 # doctest: +SKIP
|
||||||
|
1 2 [3.0, 4.0] 85.0 # doctest: +SKIP
|
||||||
|
2 1 [1.0, 2.0] 145.0 # doctest: +SKIP
|
||||||
|
>>> table.delete("x = 2") # doctest: +SKIP
|
||||||
|
>>> table.search([10,10]).to_pandas() # doctest: +SKIP
|
||||||
|
x vector _distance # doctest: +SKIP
|
||||||
|
0 3 [5.0, 6.0] 41.0 # doctest: +SKIP
|
||||||
|
1 1 [1.0, 2.0] 145.0 # doctest: +SKIP
|
||||||
|
|
||||||
|
If you have a list of values to delete, you can combine them into a
|
||||||
|
stringified list and use the `IN` operator:
|
||||||
|
|
||||||
|
>>> to_remove = [1, 3] # doctest: +SKIP
|
||||||
|
>>> to_remove = ", ".join([str(v) for v in to_remove]) # doctest: +SKIP
|
||||||
|
>>> table.delete(f"x IN ({to_remove})") # doctest: +SKIP
|
||||||
|
>>> table.search([10,10]).to_pandas() # doctest: +SKIP
|
||||||
|
x vector _distance # doctest: +SKIP
|
||||||
|
0 2 [3.0, 4.0] 85.0 # doctest: +SKIP
|
||||||
|
"""
|
||||||
payload = {"predicate": predicate}
|
payload = {"predicate": predicate}
|
||||||
self._conn._loop.run_until_complete(
|
self._conn._loop.run_until_complete(
|
||||||
self._conn._client.post(f"/v1/table/{self._name}/delete/", data=payload)
|
self._conn._client.post(f"/v1/table/{self._name}/delete/", data=payload)
|
||||||
|
|||||||
@@ -16,25 +16,29 @@ from __future__ import annotations
|
|||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import timedelta
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, Iterable, List, Optional, Union
|
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
|
||||||
|
|
||||||
import lance
|
import lance
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
import pyarrow.compute as pc
|
import pyarrow.compute as pc
|
||||||
from lance import LanceDataset
|
from lance import LanceDataset
|
||||||
from lance.dataset import CleanupStats, ReaderLike
|
|
||||||
from lance.vector import vec_to_table
|
from lance.vector import vec_to_table
|
||||||
|
|
||||||
from .common import DATA, VEC, VECTOR_COLUMN_NAME
|
from .common import DATA, VEC, VECTOR_COLUMN_NAME
|
||||||
from .embeddings import EmbeddingFunctionConfig, EmbeddingFunctionRegistry
|
from .embeddings import EmbeddingFunctionConfig, EmbeddingFunctionRegistry
|
||||||
from .pydantic import LanceModel
|
from .pydantic import LanceModel
|
||||||
from .query import LanceQueryBuilder, Query
|
from .query import LanceQueryBuilder, Query
|
||||||
from .util import fs_from_uri, safe_import_pandas
|
from .util import fs_from_uri, safe_import_pandas, value_to_sql
|
||||||
from .utils.events import register_event
|
from .utils.events import register_event
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from lance.dataset import CleanupStats, ReaderLike
|
||||||
|
|
||||||
|
|
||||||
pd = safe_import_pandas()
|
pd = safe_import_pandas()
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +90,9 @@ def _append_vector_col(data: pa.Table, metadata: dict, schema: Optional[pa.Schem
|
|||||||
for vector_column, conf in functions.items():
|
for vector_column, conf in functions.items():
|
||||||
func = conf.function
|
func = conf.function
|
||||||
if vector_column not in data.column_names:
|
if vector_column not in data.column_names:
|
||||||
col_data = func.compute_source_embeddings(data[conf.source_column])
|
col_data = func.compute_source_embeddings_with_retry(
|
||||||
|
data[conf.source_column]
|
||||||
|
)
|
||||||
if schema is not None:
|
if schema is not None:
|
||||||
dtype = schema.field(vector_column).type
|
dtype = schema.field(vector_column).type
|
||||||
else:
|
else:
|
||||||
@@ -149,13 +155,13 @@ class Table(ABC):
|
|||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def schema(self) -> pa.Schema:
|
def schema(self) -> pa.Schema:
|
||||||
"""The [Arrow Schema](https://arrow.apache.org/docs/python/api/datatypes.html#) of
|
"""The [Arrow Schema](https://arrow.apache.org/docs/python/api/datatypes.html#)
|
||||||
this Table
|
of this Table
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def to_pandas(self):
|
def to_pandas(self) -> "pd.DataFrame":
|
||||||
"""Return the table as a pandas DataFrame.
|
"""Return the table as a pandas DataFrame.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -182,6 +188,7 @@ class Table(ABC):
|
|||||||
vector_column_name: str = VECTOR_COLUMN_NAME,
|
vector_column_name: str = VECTOR_COLUMN_NAME,
|
||||||
replace: bool = True,
|
replace: bool = True,
|
||||||
accelerator: Optional[str] = None,
|
accelerator: Optional[str] = None,
|
||||||
|
index_cache_size: Optional[int] = None,
|
||||||
):
|
):
|
||||||
"""Create an index on the table.
|
"""Create an index on the table.
|
||||||
|
|
||||||
@@ -191,20 +198,23 @@ class Table(ABC):
|
|||||||
The distance metric to use when creating the index.
|
The distance metric to use when creating the index.
|
||||||
Valid values are "L2", "cosine", or "dot".
|
Valid values are "L2", "cosine", or "dot".
|
||||||
L2 is euclidean distance.
|
L2 is euclidean distance.
|
||||||
num_partitions: int
|
num_partitions: int, default 256
|
||||||
The number of IVF partitions to use when creating the index.
|
The number of IVF partitions to use when creating the index.
|
||||||
Default is 256.
|
Default is 256.
|
||||||
num_sub_vectors: int
|
num_sub_vectors: int, default 96
|
||||||
The number of PQ sub-vectors to use when creating the index.
|
The number of PQ sub-vectors to use when creating the index.
|
||||||
Default is 96.
|
Default is 96.
|
||||||
vector_column_name: str, default "vector"
|
vector_column_name: str, default "vector"
|
||||||
The vector column name to create the index.
|
The vector column name to create the index.
|
||||||
replace: bool, default True
|
replace: bool, default True
|
||||||
If True, replace the existing index if it exists.
|
- If True, replace the existing index if it exists.
|
||||||
If False, raise an error if duplicate index exists.
|
|
||||||
|
- If False, raise an error if duplicate index exists.
|
||||||
accelerator: str, default None
|
accelerator: str, default None
|
||||||
If set, use the given accelerator to create the index.
|
If set, use the given accelerator to create the index.
|
||||||
Only support "cuda" for now.
|
Only support "cuda" for now.
|
||||||
|
index_cache_size : int, optional
|
||||||
|
The size of the index cache in number of entries. Default value is 256.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -220,8 +230,14 @@ class Table(ABC):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
data: list-of-dict, dict, pd.DataFrame
|
data: DATA
|
||||||
The data to insert into the table.
|
The data to insert into the table. Acceptable types are:
|
||||||
|
|
||||||
|
- dict or list-of-dict
|
||||||
|
|
||||||
|
- pandas.DataFrame
|
||||||
|
|
||||||
|
- pyarrow.Table or pyarrow.RecordBatch
|
||||||
mode: str
|
mode: str
|
||||||
The mode to use when writing the data. Valid values are
|
The mode to use when writing the data. Valid values are
|
||||||
"append" and "overwrite".
|
"append" and "overwrite".
|
||||||
@@ -242,31 +258,70 @@ class Table(ABC):
|
|||||||
query_type: str = "auto",
|
query_type: str = "auto",
|
||||||
) -> LanceQueryBuilder:
|
) -> LanceQueryBuilder:
|
||||||
"""Create a search query to find the nearest neighbors
|
"""Create a search query to find the nearest neighbors
|
||||||
of the given query vector.
|
of the given query vector. We currently support [vector search][search]
|
||||||
|
and [full-text search][experimental-full-text-search].
|
||||||
|
|
||||||
|
All query options are defined in [Query][lancedb.query.Query].
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> import lancedb
|
||||||
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
|
>>> data = [
|
||||||
|
... {"original_width": 100, "caption": "bar", "vector": [0.1, 2.3, 4.5]},
|
||||||
|
... {"original_width": 2000, "caption": "foo", "vector": [0.5, 3.4, 1.3]},
|
||||||
|
... {"original_width": 3000, "caption": "test", "vector": [0.3, 6.2, 2.6]}
|
||||||
|
... ]
|
||||||
|
>>> table = db.create_table("my_table", data)
|
||||||
|
>>> query = [0.4, 1.4, 2.4]
|
||||||
|
>>> (table.search(query, vector_column_name="vector")
|
||||||
|
... .where("original_width > 1000", prefilter=True)
|
||||||
|
... .select(["caption", "original_width"])
|
||||||
|
... .limit(2)
|
||||||
|
... .to_pandas())
|
||||||
|
caption original_width vector _distance
|
||||||
|
0 foo 2000 [0.5, 3.4, 1.3] 5.220000
|
||||||
|
1 test 3000 [0.3, 6.2, 2.6] 23.089996
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
query: str, list, np.ndarray, PIL.Image.Image, default None
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
The query to search for. If None then
|
The targetted vector to search for.
|
||||||
the select/where/limit clauses are applied to filter
|
|
||||||
|
- *default None*.
|
||||||
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
|
|
||||||
|
- If None then the select/where/limit clauses are applied to filter
|
||||||
the table
|
the table
|
||||||
vector_column_name: str, default "vector"
|
vector_column_name: str
|
||||||
The name of the vector column to search.
|
The name of the vector column to search.
|
||||||
query_type: str, default "auto"
|
*default "vector"*
|
||||||
"vector", "fts", or "auto"
|
query_type: str
|
||||||
If "auto" then the query type is inferred from the query;
|
*default "auto"*.
|
||||||
If `query` is a list/np.ndarray then the query type is "vector";
|
Acceptable types are: "vector", "fts", or "auto"
|
||||||
If `query` is a PIL.Image.Image then either do vector search
|
|
||||||
or raise an error if no corresponding embedding function is found.
|
- If "auto" then the query type is inferred from the query;
|
||||||
If `query` is a string, then the query type is "vector" if the
|
|
||||||
|
- If `query` is a list/np.ndarray then the query type is
|
||||||
|
"vector";
|
||||||
|
|
||||||
|
- If `query` is a PIL.Image.Image then either do vector search,
|
||||||
|
or raise an error if no corresponding embedding function is found.
|
||||||
|
|
||||||
|
- If `query` is a string, then the query type is "vector" if the
|
||||||
table has embedding functions else the query type is "fts"
|
table has embedding functions else the query type is "fts"
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
LanceQueryBuilder
|
LanceQueryBuilder
|
||||||
A query builder object representing the query.
|
A query builder object representing the query.
|
||||||
Once executed, the query returns selected columns, the vector,
|
Once executed, the query returns
|
||||||
and also the "_distance" column which is the distance between the query
|
|
||||||
|
- selected columns
|
||||||
|
|
||||||
|
- the vector
|
||||||
|
|
||||||
|
- and also the "_distance" column which is the distance between the query
|
||||||
vector and the returned vector.
|
vector and the returned vector.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -285,14 +340,19 @@ class Table(ABC):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
where: str
|
where: str
|
||||||
The SQL where clause to use when deleting rows. For example, 'x = 2'
|
The SQL where clause to use when deleting rows.
|
||||||
or 'x IN (1, 2, 3)'. The filter must not be empty, or it will error.
|
|
||||||
|
- For example, 'x = 2' or 'x IN (1, 2, 3)'.
|
||||||
|
|
||||||
|
The filter must not be empty, or it will error.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> import lancedb
|
>>> import lancedb
|
||||||
>>> data = [
|
>>> data = [
|
||||||
... {"x": 1, "vector": [1, 2]}, {"x": 2, "vector": [3, 4]}, {"x": 3, "vector": [5, 6]}
|
... {"x": 1, "vector": [1, 2]},
|
||||||
|
... {"x": 2, "vector": [3, 4]},
|
||||||
|
... {"x": 3, "vector": [5, 6]}
|
||||||
... ]
|
... ]
|
||||||
>>> db = lancedb.connect("./.lancedb")
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
>>> table = db.create_table("my_table", data)
|
>>> table = db.create_table("my_table", data)
|
||||||
@@ -377,7 +437,8 @@ class LanceTable(Table):
|
|||||||
--------
|
--------
|
||||||
>>> import lancedb
|
>>> import lancedb
|
||||||
>>> db = lancedb.connect("./.lancedb")
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
>>> table = db.create_table("my_table", [{"vector": [1.1, 0.9], "type": "vector"}])
|
>>> table = db.create_table("my_table",
|
||||||
|
... [{"vector": [1.1, 0.9], "type": "vector"}])
|
||||||
>>> table.version
|
>>> table.version
|
||||||
2
|
2
|
||||||
>>> table.to_pandas()
|
>>> table.to_pandas()
|
||||||
@@ -424,7 +485,8 @@ class LanceTable(Table):
|
|||||||
--------
|
--------
|
||||||
>>> import lancedb
|
>>> import lancedb
|
||||||
>>> db = lancedb.connect("./.lancedb")
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
>>> table = db.create_table("my_table", [{"vector": [1.1, 0.9], "type": "vector"}])
|
>>> table = db.create_table("my_table", [
|
||||||
|
... {"vector": [1.1, 0.9], "type": "vector"}])
|
||||||
>>> table.version
|
>>> table.version
|
||||||
2
|
2
|
||||||
>>> table.to_pandas()
|
>>> table.to_pandas()
|
||||||
@@ -497,6 +559,7 @@ class LanceTable(Table):
|
|||||||
vector_column_name=VECTOR_COLUMN_NAME,
|
vector_column_name=VECTOR_COLUMN_NAME,
|
||||||
replace: bool = True,
|
replace: bool = True,
|
||||||
accelerator: Optional[str] = None,
|
accelerator: Optional[str] = None,
|
||||||
|
index_cache_size: Optional[int] = None,
|
||||||
):
|
):
|
||||||
"""Create an index on the table."""
|
"""Create an index on the table."""
|
||||||
self._dataset.create_index(
|
self._dataset.create_index(
|
||||||
@@ -507,6 +570,7 @@ class LanceTable(Table):
|
|||||||
num_sub_vectors=num_sub_vectors,
|
num_sub_vectors=num_sub_vectors,
|
||||||
replace=replace,
|
replace=replace,
|
||||||
accelerator=accelerator,
|
accelerator=accelerator,
|
||||||
|
index_cache_size=index_cache_size,
|
||||||
)
|
)
|
||||||
self._reset_dataset()
|
self._reset_dataset()
|
||||||
register_event("create_index")
|
register_event("create_index")
|
||||||
@@ -669,14 +733,39 @@ class LanceTable(Table):
|
|||||||
query_type: str = "auto",
|
query_type: str = "auto",
|
||||||
) -> LanceQueryBuilder:
|
) -> LanceQueryBuilder:
|
||||||
"""Create a search query to find the nearest neighbors
|
"""Create a search query to find the nearest neighbors
|
||||||
of the given query vector.
|
of the given query vector. We currently support [vector search][search]
|
||||||
|
and [full-text search][search].
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> import lancedb
|
||||||
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
|
>>> data = [
|
||||||
|
... {"original_width": 100, "caption": "bar", "vector": [0.1, 2.3, 4.5]},
|
||||||
|
... {"original_width": 2000, "caption": "foo", "vector": [0.5, 3.4, 1.3]},
|
||||||
|
... {"original_width": 3000, "caption": "test", "vector": [0.3, 6.2, 2.6]}
|
||||||
|
... ]
|
||||||
|
>>> table = db.create_table("my_table", data)
|
||||||
|
>>> query = [0.4, 1.4, 2.4]
|
||||||
|
>>> (table.search(query, vector_column_name="vector")
|
||||||
|
... .where("original_width > 1000", prefilter=True)
|
||||||
|
... .select(["caption", "original_width"])
|
||||||
|
... .limit(2)
|
||||||
|
... .to_pandas())
|
||||||
|
caption original_width vector _distance
|
||||||
|
0 foo 2000 [0.5, 3.4, 1.3] 5.220000
|
||||||
|
1 test 3000 [0.3, 6.2, 2.6] 23.089996
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
query: str, list, np.ndarray, a PIL Image or None
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
The query to search for. If None then
|
The targetted vector to search for.
|
||||||
the select/where/limit clauses are applied to filter
|
|
||||||
the table
|
- *default None*.
|
||||||
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
|
|
||||||
|
- If None then the select/[where][sql]/limit clauses are applied
|
||||||
|
to filter the table
|
||||||
vector_column_name: str, default "vector"
|
vector_column_name: str, default "vector"
|
||||||
The name of the vector column to search.
|
The name of the vector column to search.
|
||||||
query_type: str, default "auto"
|
query_type: str, default "auto"
|
||||||
@@ -685,7 +774,7 @@ class LanceTable(Table):
|
|||||||
If `query` is a list/np.ndarray then the query type is "vector";
|
If `query` is a list/np.ndarray then the query type is "vector";
|
||||||
If `query` is a PIL.Image.Image then either do vector search
|
If `query` is a PIL.Image.Image then either do vector search
|
||||||
or raise an error if no corresponding embedding function is found.
|
or raise an error if no corresponding embedding function is found.
|
||||||
If the query is a string, then the query type is "vector" if the
|
If the `query` is a string, then the query type is "vector" if the
|
||||||
table has embedding functions, else the query type is "fts"
|
table has embedding functions, else the query type is "fts"
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -696,7 +785,7 @@ class LanceTable(Table):
|
|||||||
and also the "_distance" column which is the distance between the query
|
and also the "_distance" column which is the distance between the query
|
||||||
vector and the returned vector.
|
vector and the returned vector.
|
||||||
"""
|
"""
|
||||||
register_event("search")
|
register_event("search_table")
|
||||||
return LanceQueryBuilder.create(
|
return LanceQueryBuilder.create(
|
||||||
self, query, query_type, vector_column_name=vector_column_name
|
self, query, query_type, vector_column_name=vector_column_name
|
||||||
)
|
)
|
||||||
@@ -720,7 +809,9 @@ class LanceTable(Table):
|
|||||||
--------
|
--------
|
||||||
>>> import lancedb
|
>>> import lancedb
|
||||||
>>> data = [
|
>>> data = [
|
||||||
... {"x": 1, "vector": [1, 2]}, {"x": 2, "vector": [3, 4]}, {"x": 3, "vector": [5, 6]}
|
... {"x": 1, "vector": [1, 2]},
|
||||||
|
... {"x": 2, "vector": [3, 4]},
|
||||||
|
... {"x": 3, "vector": [5, 6]}
|
||||||
... ]
|
... ]
|
||||||
>>> db = lancedb.connect("./.lancedb")
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
>>> table = db.create_table("my_table", data)
|
>>> table = db.create_table("my_table", data)
|
||||||
@@ -740,7 +831,8 @@ class LanceTable(Table):
|
|||||||
The data to insert into the table.
|
The data to insert into the table.
|
||||||
At least one of `data` or `schema` must be provided.
|
At least one of `data` or `schema` must be provided.
|
||||||
schema: pa.Schema or LanceModel, optional
|
schema: pa.Schema or LanceModel, optional
|
||||||
The schema of the table. If not provided, the schema is inferred from the data.
|
The schema of the table. If not provided,
|
||||||
|
the schema is inferred from the data.
|
||||||
At least one of `data` or `schema` must be provided.
|
At least one of `data` or `schema` must be provided.
|
||||||
mode: str, default "create"
|
mode: str, default "create"
|
||||||
The mode to use when writing the data. Valid values are
|
The mode to use when writing the data. Valid values are
|
||||||
@@ -811,35 +903,45 @@ class LanceTable(Table):
|
|||||||
file_info = fs.get_file_info(path)
|
file_info = fs.get_file_info(path)
|
||||||
if file_info.type != pa.fs.FileType.Directory:
|
if file_info.type != pa.fs.FileType.Directory:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Table {name} does not exist. Please first call db.create_table({name}, data)"
|
f"Table {name} does not exist."
|
||||||
|
f"Please first call db.create_table({name}, data)"
|
||||||
)
|
)
|
||||||
|
register_event("open_table")
|
||||||
|
|
||||||
return tbl
|
return tbl
|
||||||
|
|
||||||
def delete(self, where: str):
|
def delete(self, where: str):
|
||||||
self._dataset.delete(where)
|
self._dataset.delete(where)
|
||||||
|
|
||||||
def update(self, where: str, values: dict):
|
def update(
|
||||||
|
self,
|
||||||
|
where: Optional[str] = None,
|
||||||
|
values: Optional[dict] = None,
|
||||||
|
*,
|
||||||
|
values_sql: Optional[Dict[str, str]] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
EXPERIMENTAL: Update rows in the table (not threadsafe).
|
|
||||||
|
|
||||||
This can be used to update zero to all rows depending on how many
|
This can be used to update zero to all rows depending on how many
|
||||||
rows match the where clause.
|
rows match the where clause.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
where: str
|
where: str, optional
|
||||||
The SQL where clause to use when updating rows. For example, 'x = 2'
|
The SQL where clause to use when updating rows. For example, 'x = 2'
|
||||||
or 'x IN (1, 2, 3)'. The filter must not be empty, or it will error.
|
or 'x IN (1, 2, 3)'. The filter must not be empty, or it will error.
|
||||||
values: dict
|
values: dict, optional
|
||||||
The values to update. The keys are the column names and the values
|
The values to update. The keys are the column names and the values
|
||||||
are the values to set.
|
are the values to set.
|
||||||
|
values_sql: dict, optional
|
||||||
|
The values to update, expressed as SQL expression strings. These can
|
||||||
|
reference existing columns. For example, {"x": "x + 1"} will increment
|
||||||
|
the x column by 1.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> import lancedb
|
>>> import lancedb
|
||||||
>>> data = [
|
>>> import pandas as pd
|
||||||
... {"x": 1, "vector": [1, 2]}, {"x": 2, "vector": [3, 4]}, {"x": 3, "vector": [5, 6]}
|
>>> data = pd.DataFrame({"x": [1, 2, 3], "vector": [[1, 2], [3, 4], [5, 6]]})
|
||||||
... ]
|
|
||||||
>>> db = lancedb.connect("./.lancedb")
|
>>> db = lancedb.connect("./.lancedb")
|
||||||
>>> table = db.create_table("my_table", data)
|
>>> table = db.create_table("my_table", data)
|
||||||
>>> table.to_pandas()
|
>>> table.to_pandas()
|
||||||
@@ -855,29 +957,20 @@ class LanceTable(Table):
|
|||||||
2 2 [10.0, 10.0]
|
2 2 [10.0, 10.0]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
orig_data = self._dataset.to_table(filter=where).combine_chunks()
|
if values is not None and values_sql is not None:
|
||||||
if len(orig_data) == 0:
|
raise ValueError("Only one of values or values_sql can be provided")
|
||||||
return
|
if values is None and values_sql is None:
|
||||||
for col, val in values.items():
|
raise ValueError("Either values or values_sql must be provided")
|
||||||
i = orig_data.column_names.index(col)
|
|
||||||
if i < 0:
|
if values is not None:
|
||||||
raise ValueError(f"Column {col} does not exist")
|
values_sql = {k: value_to_sql(v) for k, v in values.items()}
|
||||||
orig_data = orig_data.set_column(
|
|
||||||
i, col, pa.array([val] * len(orig_data), type=orig_data[col].type)
|
self.to_lance().update(values_sql, where)
|
||||||
)
|
|
||||||
self.delete(where)
|
|
||||||
self.add(orig_data, mode="append")
|
|
||||||
self._reset_dataset()
|
self._reset_dataset()
|
||||||
register_event("update")
|
register_event("update")
|
||||||
|
|
||||||
def _execute_query(self, query: Query) -> pa.Table:
|
def _execute_query(self, query: Query) -> pa.Table:
|
||||||
ds = self.to_lance()
|
ds = self.to_lance()
|
||||||
if query.prefilter:
|
|
||||||
for idx in ds.list_indices():
|
|
||||||
if query.vector_column in idx["fields"]:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Prefiltering for indexed vector column is coming soon."
|
|
||||||
)
|
|
||||||
return ds.to_table(
|
return ds.to_table(
|
||||||
columns=query.columns,
|
columns=query.columns,
|
||||||
filter=query.filter,
|
filter=query.filter,
|
||||||
@@ -1019,7 +1112,8 @@ def _sanitize_vector_column(
|
|||||||
# ChunkedArray is annoying to work with, so we combine chunks here
|
# ChunkedArray is annoying to work with, so we combine chunks here
|
||||||
vec_arr = data[vector_column_name].combine_chunks()
|
vec_arr = data[vector_column_name].combine_chunks()
|
||||||
if pa.types.is_list(data[vector_column_name].type):
|
if pa.types.is_list(data[vector_column_name].type):
|
||||||
# if it's a variable size list array we make sure the dimensions are all the same
|
# if it's a variable size list array,
|
||||||
|
# we make sure the dimensions are all the same
|
||||||
has_jagged_ndims = len(vec_arr.values) % len(data) != 0
|
has_jagged_ndims = len(vec_arr.values) % len(data) != 0
|
||||||
if has_jagged_ndims:
|
if has_jagged_ndims:
|
||||||
data = _sanitize_jagged(
|
data = _sanitize_jagged(
|
||||||
|
|||||||
@@ -12,9 +12,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from datetime import date, datetime
|
||||||
|
from functools import singledispatch
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pyarrow.fs as pa_fs
|
import pyarrow.fs as pa_fs
|
||||||
|
|
||||||
|
|
||||||
@@ -88,3 +91,53 @@ def safe_import_pandas():
|
|||||||
return pd
|
return pd
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def value_to_sql(value):
|
||||||
|
raise NotImplementedError("SQL conversion is not implemented for this type")
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(str)
|
||||||
|
def _(value: str):
|
||||||
|
return f"'{value}'"
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(int)
|
||||||
|
def _(value: int):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(float)
|
||||||
|
def _(value: float):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(bool)
|
||||||
|
def _(value: bool):
|
||||||
|
return str(value).upper()
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(type(None))
|
||||||
|
def _(value: type(None)):
|
||||||
|
return "NULL"
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(datetime)
|
||||||
|
def _(value: datetime):
|
||||||
|
return f"'{value.isoformat()}'"
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(date)
|
||||||
|
def _(value: date):
|
||||||
|
return f"'{value.isoformat()}'"
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(list)
|
||||||
|
def _(value: list):
|
||||||
|
return "[" + ", ".join(map(value_to_sql, value)) + "]"
|
||||||
|
|
||||||
|
|
||||||
|
@value_to_sql.register(np.ndarray)
|
||||||
|
def _(value: np.ndarray):
|
||||||
|
return value_to_sql(value.tolist())
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ class _Events:
|
|||||||
Initializes the Events object with default values for events, rate_limit, and metadata.
|
Initializes the Events object with default values for events, rate_limit, and metadata.
|
||||||
"""
|
"""
|
||||||
self.events = [] # events list
|
self.events = [] # events list
|
||||||
self.max_events = 25 # max events to store in memory
|
self.throttled_event_names = ["search_table"]
|
||||||
self.rate_limit = 60.0 # rate limit (seconds)
|
self.throttled_events = set()
|
||||||
|
self.max_events = 5 # max events to store in memory
|
||||||
|
self.rate_limit = 60.0 * 5 # rate limit (seconds)
|
||||||
self.time = 0.0
|
self.time = 0.0
|
||||||
|
|
||||||
if is_git_dir():
|
if is_git_dir():
|
||||||
@@ -112,18 +114,21 @@ class _Events:
|
|||||||
return
|
return
|
||||||
if (
|
if (
|
||||||
len(self.events) < self.max_events
|
len(self.events) < self.max_events
|
||||||
): # Events list limited to 25 events (drop any events past this)
|
): # Events list limited to self.max_events (drop any events past this)
|
||||||
params.update(self.metadata)
|
params.update(self.metadata)
|
||||||
self.events.append(
|
event = {
|
||||||
{
|
"event": event_name,
|
||||||
"event": event_name,
|
"properties": params,
|
||||||
"properties": params,
|
"timestamp": datetime.datetime.now(
|
||||||
"timestamp": datetime.datetime.now(
|
tz=datetime.timezone.utc
|
||||||
tz=datetime.timezone.utc
|
).isoformat(),
|
||||||
).isoformat(),
|
"distinct_id": CONFIG["uuid"],
|
||||||
"distinct_id": CONFIG["uuid"],
|
}
|
||||||
}
|
if event_name not in self.throttled_event_names:
|
||||||
)
|
self.events.append(event)
|
||||||
|
elif event_name not in self.throttled_events:
|
||||||
|
self.throttled_events.add(event_name)
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
# Check rate limit
|
# Check rate limit
|
||||||
t = time.time()
|
t = time.time()
|
||||||
@@ -135,7 +140,6 @@ class _Events:
|
|||||||
"distinct_id": CONFIG["uuid"], # posthog needs this to accepts the event
|
"distinct_id": CONFIG["uuid"], # posthog needs this to accepts the event
|
||||||
"batch": self.events,
|
"batch": self.events,
|
||||||
}
|
}
|
||||||
|
|
||||||
# POST equivalent to requests.post(self.url, json=data).
|
# POST equivalent to requests.post(self.url, json=data).
|
||||||
# threaded request is used to avoid blocking, retries are disabled, and verbose is disabled
|
# threaded request is used to avoid blocking, retries are disabled, and verbose is disabled
|
||||||
# to avoid any possible disruption in the console.
|
# to avoid any possible disruption in the console.
|
||||||
@@ -150,6 +154,7 @@ class _Events:
|
|||||||
|
|
||||||
# Flush & Reset
|
# Flush & Reset
|
||||||
self.events = []
|
self.events = []
|
||||||
|
self.throttled_events = set()
|
||||||
self.time = t
|
self.time = t
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ def set_sentry():
|
|||||||
"""
|
"""
|
||||||
if "exc_info" in hint:
|
if "exc_info" in hint:
|
||||||
exc_type, exc_value, tb = hint["exc_info"]
|
exc_type, exc_value, tb = hint["exc_info"]
|
||||||
if "out of memory" in str(exc_value).lower():
|
ignored_errors = ["out of memory", "no space left on device", "testing"]
|
||||||
|
if any(error in str(exc_value).lower() for error in ignored_errors):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if is_git_dir():
|
if is_git_dir():
|
||||||
@@ -97,7 +98,7 @@ def set_sentry():
|
|||||||
dsn="https://c63ef8c64e05d1aa1a96513361f3ca2f@o4505950840946688.ingest.sentry.io/4505950933614592",
|
dsn="https://c63ef8c64e05d1aa1a96513361f3ca2f@o4505950840946688.ingest.sentry.io/4505950933614592",
|
||||||
debug=False,
|
debug=False,
|
||||||
include_local_variables=False,
|
include_local_variables=False,
|
||||||
traces_sample_rate=1.0,
|
traces_sample_rate=0.5,
|
||||||
environment="production", # 'dev' or 'production'
|
environment="production", # 'dev' or 'production'
|
||||||
before_send=before_send,
|
before_send=before_send,
|
||||||
ignore_errors=[KeyboardInterrupt, FileNotFoundError, bdb.BdbQuit],
|
ignore_errors=[KeyboardInterrupt, FileNotFoundError, bdb.BdbQuit],
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lancedb"
|
name = "lancedb"
|
||||||
version = "0.3.2"
|
version = "0.3.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deprecation",
|
"deprecation",
|
||||||
"pylance==0.8.7",
|
"pylance==0.8.21",
|
||||||
"ratelimiter~=1.0",
|
"ratelimiter~=1.0",
|
||||||
"retry>=0.9.2",
|
"retry>=0.9.2",
|
||||||
"tqdm>=4.1.0",
|
"tqdm>=4.27.0",
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
"pydantic>=1.10",
|
"pydantic>=1.10",
|
||||||
"attrs>=21.3.0",
|
"attrs>=21.3.0",
|
||||||
@@ -14,7 +14,8 @@ dependencies = [
|
|||||||
"cachetools",
|
"cachetools",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"click>=8.1.7",
|
"click>=8.1.7",
|
||||||
"requests>=2.31.0"
|
"requests>=2.31.0",
|
||||||
|
"overrides>=0.7"
|
||||||
]
|
]
|
||||||
description = "lancedb"
|
description = "lancedb"
|
||||||
authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }]
|
authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }]
|
||||||
@@ -52,7 +53,7 @@ tests = ["pandas>=1.4", "pytest", "pytest-mock", "pytest-asyncio", "requests"]
|
|||||||
dev = ["ruff", "pre-commit", "black"]
|
dev = ["ruff", "pre-commit", "black"]
|
||||||
docs = ["mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocstrings[python]"]
|
docs = ["mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocstrings[python]"]
|
||||||
clip = ["torch", "pillow", "open-clip"]
|
clip = ["torch", "pillow", "open-clip"]
|
||||||
embeddings = ["openai", "sentence-transformers", "torch", "pillow", "open-clip-torch", "cohere"]
|
embeddings = ["openai", "sentence-transformers", "torch", "pillow", "open-clip-torch", "cohere", "InstructorEmbedding"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
lancedb = "lancedb.cli.cli:cli"
|
lancedb = "lancedb.cli.cli:cli"
|
||||||
@@ -64,6 +65,9 @@ build-backend = "setuptools.build_meta"
|
|||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
select = ["F", "E", "W", "I", "G", "TCH", "PERF"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--strict-markers"
|
addopts = "--strict-markers"
|
||||||
markers = [
|
markers = [
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def test_ingest_iterator(tmp_path):
|
|||||||
[
|
[
|
||||||
PydanticSchema(vector=[3.1, 4.1], item="foo", price=10.0),
|
PydanticSchema(vector=[3.1, 4.1], item="foo", price=10.0),
|
||||||
PydanticSchema(vector=[5.9, 26.5], item="bar", price=20.0),
|
PydanticSchema(vector=[5.9, 26.5], item="bar", price=20.0),
|
||||||
]
|
],
|
||||||
# TODO: test pydict separately. it is unique column number and names contraint
|
# TODO: test pydict separately. it is unique column number and names contraint
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -150,6 +150,21 @@ def test_ingest_iterator(tmp_path):
|
|||||||
run_tests(PydanticSchema)
|
run_tests(PydanticSchema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_names(tmp_path):
|
||||||
|
db = lancedb.connect(tmp_path)
|
||||||
|
data = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"vector": [[3.1, 4.1], [5.9, 26.5]],
|
||||||
|
"item": ["foo", "bar"],
|
||||||
|
"price": [10.0, 20.0],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.create_table("test2", data=data)
|
||||||
|
db.create_table("test1", data=data)
|
||||||
|
db.create_table("test3", data=data)
|
||||||
|
assert db.table_names() == ["test1", "test2", "test3"]
|
||||||
|
|
||||||
|
|
||||||
def test_create_mode(tmp_path):
|
def test_create_mode(tmp_path):
|
||||||
db = lancedb.connect(tmp_path)
|
db = lancedb.connect(tmp_path)
|
||||||
data = pd.DataFrame(
|
data = pd.DataFrame(
|
||||||
@@ -286,4 +301,29 @@ def test_replace_index(tmp_path):
|
|||||||
num_partitions=2,
|
num_partitions=2,
|
||||||
num_sub_vectors=4,
|
num_sub_vectors=4,
|
||||||
replace=True,
|
replace=True,
|
||||||
|
index_cache_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefilter_with_index(tmp_path):
|
||||||
|
db = lancedb.connect(uri=tmp_path)
|
||||||
|
data = [
|
||||||
|
{"vector": np.random.rand(128), "item": "foo", "price": float(i)}
|
||||||
|
for i in range(1000)
|
||||||
|
]
|
||||||
|
sample_key = data[100]["vector"]
|
||||||
|
table = db.create_table(
|
||||||
|
"test",
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
table.create_index(
|
||||||
|
num_partitions=2,
|
||||||
|
num_sub_vectors=4,
|
||||||
|
)
|
||||||
|
table = (
|
||||||
|
table.search(sample_key)
|
||||||
|
.where("price == 500", prefilter=True)
|
||||||
|
.limit(5)
|
||||||
|
.to_arrow()
|
||||||
|
)
|
||||||
|
assert table.num_rows == 1
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ import sys
|
|||||||
import lance
|
import lance
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
|
import pytest
|
||||||
|
|
||||||
from lancedb.conftest import MockTextEmbeddingFunction
|
import lancedb
|
||||||
|
from lancedb.conftest import MockRateLimitedEmbeddingFunction, MockTextEmbeddingFunction
|
||||||
from lancedb.embeddings import (
|
from lancedb.embeddings import (
|
||||||
EmbeddingFunctionConfig,
|
EmbeddingFunctionConfig,
|
||||||
EmbeddingFunctionRegistry,
|
EmbeddingFunctionRegistry,
|
||||||
with_embeddings,
|
with_embeddings,
|
||||||
)
|
)
|
||||||
|
from lancedb.pydantic import LanceModel, Vector
|
||||||
|
|
||||||
|
|
||||||
def mock_embed_func(input_data):
|
def mock_embed_func(input_data):
|
||||||
@@ -83,3 +86,29 @@ def test_embedding_function(tmp_path):
|
|||||||
expected = func.compute_query_embeddings("hello world")
|
expected = func.compute_query_embeddings("hello world")
|
||||||
|
|
||||||
assert np.allclose(actual, expected)
|
assert np.allclose(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_embedding_function_rate_limit(tmp_path):
|
||||||
|
def _get_schema_from_model(model):
|
||||||
|
class Schema(LanceModel):
|
||||||
|
text: str = model.SourceField()
|
||||||
|
vector: Vector(model.ndims()) = model.VectorField()
|
||||||
|
|
||||||
|
return Schema
|
||||||
|
|
||||||
|
db = lancedb.connect(tmp_path)
|
||||||
|
registry = EmbeddingFunctionRegistry.get_instance()
|
||||||
|
model = registry.get("test-rate-limited").create(max_retries=0)
|
||||||
|
schema = _get_schema_from_model(model)
|
||||||
|
table = db.create_table("test", schema=schema, mode="overwrite")
|
||||||
|
table.add([{"text": "hello world"}])
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
table.add([{"text": "hello world"}])
|
||||||
|
assert len(table) == 1
|
||||||
|
|
||||||
|
model = registry.get("test-rate-limited").create()
|
||||||
|
schema = _get_schema_from_model(model)
|
||||||
|
table = db.create_table("test", schema=schema, mode="overwrite")
|
||||||
|
table.add([{"text": "hello world"}])
|
||||||
|
table.add([{"text": "hello world"}])
|
||||||
|
assert len(table) == 2
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ from lancedb.pydantic import LanceModel, Vector
|
|||||||
def test_sentence_transformer(alias, tmp_path):
|
def test_sentence_transformer(alias, tmp_path):
|
||||||
db = lancedb.connect(tmp_path)
|
db = lancedb.connect(tmp_path)
|
||||||
registry = get_registry()
|
registry = get_registry()
|
||||||
func = registry.get(alias).create()
|
func = registry.get(alias).create(max_retries=0)
|
||||||
func2 = registry.get(alias).create()
|
func2 = registry.get(alias).create(max_retries=0)
|
||||||
|
|
||||||
class Words(LanceModel):
|
class Words(LanceModel):
|
||||||
text: str = func.SourceField()
|
text: str = func.SourceField()
|
||||||
@@ -150,7 +150,11 @@ def test_openclip(tmp_path):
|
|||||||
os.environ.get("COHERE_API_KEY") is None, reason="COHERE_API_KEY not set"
|
os.environ.get("COHERE_API_KEY") is None, reason="COHERE_API_KEY not set"
|
||||||
) # also skip if cohere not installed
|
) # also skip if cohere not installed
|
||||||
def test_cohere_embedding_function():
|
def test_cohere_embedding_function():
|
||||||
cohere = get_registry().get("cohere").create(name="embed-multilingual-v2.0")
|
cohere = (
|
||||||
|
get_registry()
|
||||||
|
.get("cohere")
|
||||||
|
.create(name="embed-multilingual-v2.0", max_retries=0)
|
||||||
|
)
|
||||||
|
|
||||||
class TextModel(LanceModel):
|
class TextModel(LanceModel):
|
||||||
text: str = cohere.SourceField()
|
text: str = cohere.SourceField()
|
||||||
@@ -162,3 +166,19 @@ def test_cohere_embedding_function():
|
|||||||
|
|
||||||
tbl.add(df)
|
tbl.add(df)
|
||||||
assert len(tbl.to_pandas()["vector"][0]) == cohere.ndims()
|
assert len(tbl.to_pandas()["vector"][0]) == cohere.ndims()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_instructor_embedding(tmp_path):
|
||||||
|
model = get_registry().get("instructor").create()
|
||||||
|
|
||||||
|
class TextModel(LanceModel):
|
||||||
|
text: str = model.SourceField()
|
||||||
|
vector: Vector(model.ndims()) = model.VectorField()
|
||||||
|
|
||||||
|
df = pd.DataFrame({"text": ["hello world", "goodbye world"]})
|
||||||
|
db = lancedb.connect(tmp_path)
|
||||||
|
tbl = db.create_table("test", schema=TextModel, mode="overwrite")
|
||||||
|
|
||||||
|
tbl.add(df)
|
||||||
|
assert len(tbl.to_pandas()["vector"][0]) == model.ndims()
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ class FakeLanceDBClient:
|
|||||||
t = pa.schema([]).empty_table()
|
t = pa.schema([]).empty_table()
|
||||||
return VectorQueryResult(t)
|
return VectorQueryResult(t)
|
||||||
|
|
||||||
|
async def post(self, path: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_remote_db():
|
def test_remote_db():
|
||||||
conn = lancedb.connect("db://client-will-be-injected", api_key="fake")
|
conn = lancedb.connect("db://client-will-be-injected", api_key="fake")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
from datetime import timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
from unittest.mock import PropertyMock, patch
|
from unittest.mock import PropertyMock, patch
|
||||||
@@ -213,6 +213,7 @@ def test_create_index_method():
|
|||||||
num_sub_vectors=96,
|
num_sub_vectors=96,
|
||||||
vector_column_name="vector",
|
vector_column_name="vector",
|
||||||
replace=True,
|
replace=True,
|
||||||
|
index_cache_size=256,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the _dataset.create_index method was called
|
# Check that the _dataset.create_index method was called
|
||||||
@@ -225,6 +226,7 @@ def test_create_index_method():
|
|||||||
num_sub_vectors=96,
|
num_sub_vectors=96,
|
||||||
replace=True,
|
replace=True,
|
||||||
accelerator=None,
|
accelerator=None,
|
||||||
|
index_cache_size=256,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -346,14 +348,79 @@ def test_update(db):
|
|||||||
assert len(table) == 2
|
assert len(table) == 2
|
||||||
assert len(table.list_versions()) == 2
|
assert len(table.list_versions()) == 2
|
||||||
table.update(where="id=0", values={"vector": [1.1, 1.1]})
|
table.update(where="id=0", values={"vector": [1.1, 1.1]})
|
||||||
assert len(table.list_versions()) == 4
|
assert len(table.list_versions()) == 3
|
||||||
assert table.version == 4
|
assert table.version == 3
|
||||||
assert len(table) == 2
|
assert len(table) == 2
|
||||||
v = table.to_arrow()["vector"].combine_chunks()
|
v = table.to_arrow()["vector"].combine_chunks()
|
||||||
v = v.values.to_numpy().reshape(2, 2)
|
v = v.values.to_numpy().reshape(2, 2)
|
||||||
assert np.allclose(v, np.array([[1.2, 1.9], [1.1, 1.1]]))
|
assert np.allclose(v, np.array([[1.2, 1.9], [1.1, 1.1]]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_types(db):
|
||||||
|
table = LanceTable.create(
|
||||||
|
db,
|
||||||
|
"my_table",
|
||||||
|
data=[
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"str": "foo",
|
||||||
|
"float": 1.1,
|
||||||
|
"timestamp": datetime(2021, 1, 1),
|
||||||
|
"date": date(2021, 1, 1),
|
||||||
|
"vector1": [1.0, 0.0],
|
||||||
|
"vector2": [1.0, 1.0],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# Update with SQL
|
||||||
|
table.update(
|
||||||
|
values_sql=dict(
|
||||||
|
id="1",
|
||||||
|
str="'bar'",
|
||||||
|
float="2.2",
|
||||||
|
timestamp="TIMESTAMP '2021-01-02 00:00:00'",
|
||||||
|
date="DATE '2021-01-02'",
|
||||||
|
vector1="[2.0, 2.0]",
|
||||||
|
vector2="[3.0, 3.0]",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
actual = table.to_arrow().to_pylist()[0]
|
||||||
|
expected = dict(
|
||||||
|
id=1,
|
||||||
|
str="bar",
|
||||||
|
float=2.2,
|
||||||
|
timestamp=datetime(2021, 1, 2),
|
||||||
|
date=date(2021, 1, 2),
|
||||||
|
vector1=[2.0, 2.0],
|
||||||
|
vector2=[3.0, 3.0],
|
||||||
|
)
|
||||||
|
assert actual == expected
|
||||||
|
|
||||||
|
# Update with values
|
||||||
|
table.update(
|
||||||
|
values=dict(
|
||||||
|
id=2,
|
||||||
|
str="baz",
|
||||||
|
float=3.3,
|
||||||
|
timestamp=datetime(2021, 1, 3),
|
||||||
|
date=date(2021, 1, 3),
|
||||||
|
vector1=[3.0, 3.0],
|
||||||
|
vector2=np.array([4.0, 4.0]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
actual = table.to_arrow().to_pylist()[0]
|
||||||
|
expected = dict(
|
||||||
|
id=2,
|
||||||
|
str="baz",
|
||||||
|
float=3.3,
|
||||||
|
timestamp=datetime(2021, 1, 3),
|
||||||
|
date=date(2021, 1, 3),
|
||||||
|
vector1=[3.0, 3.0],
|
||||||
|
vector2=[4.0, 4.0],
|
||||||
|
)
|
||||||
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
def test_create_with_embedding_function(db):
|
def test_create_with_embedding_function(db):
|
||||||
class MyTable(LanceModel):
|
class MyTable(LanceModel):
|
||||||
text: str
|
text: str
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vectordb-node"
|
name = "vectordb-node"
|
||||||
version = "0.3.3"
|
version = "0.3.9"
|
||||||
description = "Serverless, low-latency vector database for AI applications"
|
description = "Serverless, low-latency vector database for AI applications"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -19,6 +19,7 @@ once_cell = "1"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
half = { workspace = true }
|
half = { workspace = true }
|
||||||
lance = { workspace = true }
|
lance = { workspace = true }
|
||||||
|
lance-index = { workspace = true }
|
||||||
lance-linalg = { workspace = true }
|
lance-linalg = { workspace = true }
|
||||||
vectordb = { path = "../../vectordb" }
|
vectordb = { path = "../../vectordb" }
|
||||||
tokio = { version = "1.23", features = ["rt-multi-thread"] }
|
tokio = { version = "1.23", features = ["rt-multi-thread"] }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use lance::index::vector::{ivf::IvfBuildParams, pq::PQBuildParams};
|
use lance_index::vector::{ivf::IvfBuildParams, pq::PQBuildParams};
|
||||||
use lance_linalg::distance::MetricType;
|
use lance_linalg::distance::MetricType;
|
||||||
use neon::context::FunctionContext;
|
use neon::context::FunctionContext;
|
||||||
use neon::prelude::*;
|
use neon::prelude::*;
|
||||||
@@ -70,7 +70,6 @@ fn get_index_params_builder(
|
|||||||
.map(|mt| {
|
.map(|mt| {
|
||||||
let metric_type = mt.unwrap();
|
let metric_type = mt.unwrap();
|
||||||
index_builder.metric_type(metric_type);
|
index_builder.metric_type(metric_type);
|
||||||
pq_params.metric_type = metric_type;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let num_partitions = obj.get_opt_usize(cx, "num_partitions")?;
|
let num_partitions = obj.get_opt_usize(cx, "num_partitions")?;
|
||||||
|
|||||||
@@ -237,8 +237,11 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
|||||||
cx.export_function("tableAdd", JsTable::js_add)?;
|
cx.export_function("tableAdd", JsTable::js_add)?;
|
||||||
cx.export_function("tableCountRows", JsTable::js_count_rows)?;
|
cx.export_function("tableCountRows", JsTable::js_count_rows)?;
|
||||||
cx.export_function("tableDelete", JsTable::js_delete)?;
|
cx.export_function("tableDelete", JsTable::js_delete)?;
|
||||||
|
cx.export_function("tableUpdate", JsTable::js_update)?;
|
||||||
cx.export_function("tableCleanupOldVersions", JsTable::js_cleanup)?;
|
cx.export_function("tableCleanupOldVersions", JsTable::js_cleanup)?;
|
||||||
cx.export_function("tableCompactFiles", JsTable::js_compact)?;
|
cx.export_function("tableCompactFiles", JsTable::js_compact)?;
|
||||||
|
cx.export_function("tableListIndices", JsTable::js_list_indices)?;
|
||||||
|
cx.export_function("tableIndexStats", JsTable::js_index_stats)?;
|
||||||
cx.export_function(
|
cx.export_function(
|
||||||
"tableCreateVectorIndex",
|
"tableCreateVectorIndex",
|
||||||
index::vector::table_create_vector_index,
|
index::vector::table_create_vector_index,
|
||||||
|
|||||||
@@ -23,8 +23,14 @@ impl JsQuery {
|
|||||||
let query_obj = cx.argument::<JsObject>(0)?;
|
let query_obj = cx.argument::<JsObject>(0)?;
|
||||||
|
|
||||||
let limit = query_obj
|
let limit = query_obj
|
||||||
.get::<JsNumber, _, _>(&mut cx, "_limit")?
|
.get_opt::<JsNumber, _, _>(&mut cx, "_limit")?
|
||||||
.value(&mut cx);
|
.map(|value| {
|
||||||
|
let limit = value.value(&mut cx) as u64;
|
||||||
|
if limit <= 0 {
|
||||||
|
panic!("Limit must be a positive integer");
|
||||||
|
}
|
||||||
|
limit
|
||||||
|
});
|
||||||
let select = query_obj
|
let select = query_obj
|
||||||
.get_opt::<JsArray, _, _>(&mut cx, "_select")?
|
.get_opt::<JsArray, _, _>(&mut cx, "_select")?
|
||||||
.map(|arr| {
|
.map(|arr| {
|
||||||
@@ -48,6 +54,10 @@ impl JsQuery {
|
|||||||
.map(|s| s.value(&mut cx))
|
.map(|s| s.value(&mut cx))
|
||||||
.map(|s| MetricType::try_from(s.as_str()).unwrap());
|
.map(|s| MetricType::try_from(s.as_str()).unwrap());
|
||||||
|
|
||||||
|
let prefilter = query_obj
|
||||||
|
.get::<JsBoolean, _, _>(&mut cx, "_prefilter")?
|
||||||
|
.value(&mut cx);
|
||||||
|
|
||||||
let is_electron = cx
|
let is_electron = cx
|
||||||
.argument::<JsBoolean>(1)
|
.argument::<JsBoolean>(1)
|
||||||
.or_throw(&mut cx)?
|
.or_throw(&mut cx)?
|
||||||
@@ -57,19 +67,23 @@ impl JsQuery {
|
|||||||
|
|
||||||
let (deferred, promise) = cx.promise();
|
let (deferred, promise) = cx.promise();
|
||||||
let channel = cx.channel();
|
let channel = cx.channel();
|
||||||
let query_vector = query_obj.get::<JsArray, _, _>(&mut cx, "_queryVector")?;
|
let query_vector = query_obj.get_opt::<JsArray, _, _>(&mut cx, "_queryVector")?;
|
||||||
let query = convert::js_array_to_vec(query_vector.deref(), &mut cx);
|
|
||||||
let table = js_table.table.clone();
|
let table = js_table.table.clone();
|
||||||
|
let query = query_vector.map(|q| convert::js_array_to_vec(q.deref(), &mut cx));
|
||||||
|
|
||||||
rt.spawn(async move {
|
rt.spawn(async move {
|
||||||
let builder = table
|
let mut builder = table
|
||||||
.search(Float32Array::from(query))
|
.search(query.map(|q| Float32Array::from(q)))
|
||||||
.limit(limit as usize)
|
|
||||||
.refine_factor(refine_factor)
|
.refine_factor(refine_factor)
|
||||||
.nprobes(nprobes)
|
.nprobes(nprobes)
|
||||||
.filter(filter)
|
.filter(filter)
|
||||||
.metric_type(metric_type)
|
.metric_type(metric_type)
|
||||||
.select(select);
|
.select(select)
|
||||||
|
.prefilter(prefilter);
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
builder = builder.limit(limit as usize);
|
||||||
|
};
|
||||||
|
|
||||||
let record_batch_stream = builder.execute();
|
let record_batch_stream = builder.execute();
|
||||||
let results = record_batch_stream
|
let results = record_batch_stream
|
||||||
.and_then(|stream| {
|
.and_then(|stream| {
|
||||||
|
|||||||
@@ -165,6 +165,69 @@ impl JsTable {
|
|||||||
Ok(promise)
|
Ok(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn js_update(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||||
|
let js_table = cx.this().downcast_or_throw::<JsBox<JsTable>, _>(&mut cx)?;
|
||||||
|
let mut table = js_table.table.clone();
|
||||||
|
|
||||||
|
let rt = runtime(&mut cx)?;
|
||||||
|
let (deferred, promise) = cx.promise();
|
||||||
|
let channel = cx.channel();
|
||||||
|
|
||||||
|
// create a vector of updates from the passed map
|
||||||
|
let updates_arg = cx.argument::<JsObject>(1)?;
|
||||||
|
let properties = updates_arg.get_own_property_names(&mut cx)?;
|
||||||
|
let mut updates: Vec<(String, String)> =
|
||||||
|
Vec::with_capacity(properties.len(&mut cx) as usize);
|
||||||
|
|
||||||
|
let len_properties = properties.len(&mut cx);
|
||||||
|
for i in 0..len_properties {
|
||||||
|
let property = properties
|
||||||
|
.get_value(&mut cx, i)?
|
||||||
|
.downcast_or_throw::<JsString, _>(&mut cx)?;
|
||||||
|
|
||||||
|
let value = updates_arg
|
||||||
|
.get_value(&mut cx, property.clone())?
|
||||||
|
.downcast_or_throw::<JsString, _>(&mut cx)?;
|
||||||
|
|
||||||
|
let property = property.value(&mut cx);
|
||||||
|
let value = value.value(&mut cx);
|
||||||
|
updates.push((property, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the filter/predicate if the user passed one
|
||||||
|
let predicate = cx.argument_opt(0);
|
||||||
|
let predicate = predicate.unwrap().downcast::<JsString, _>(&mut cx);
|
||||||
|
let predicate = match predicate {
|
||||||
|
Ok(_) => {
|
||||||
|
let val = predicate.map(|s| s.value(&mut cx)).unwrap();
|
||||||
|
Some(val)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// if the predicate is not string, check it's null otherwise an invalid
|
||||||
|
// type was passed
|
||||||
|
cx.argument::<JsNull>(0)?;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.spawn(async move {
|
||||||
|
let updates_arg = updates
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let predicate = predicate.as_ref().map(|s| s.as_str());
|
||||||
|
|
||||||
|
let update_result = table.update(predicate, updates_arg).await;
|
||||||
|
deferred.settle_with(&channel, move |mut cx| {
|
||||||
|
update_result.or_throw(&mut cx)?;
|
||||||
|
Ok(cx.boxed(JsTable::from(table)))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(promise)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn js_cleanup(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
pub(crate) fn js_cleanup(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||||
let js_table = cx.this().downcast_or_throw::<JsBox<JsTable>, _>(&mut cx)?;
|
let js_table = cx.this().downcast_or_throw::<JsBox<JsTable>, _>(&mut cx)?;
|
||||||
let rt = runtime(&mut cx)?;
|
let rt = runtime(&mut cx)?;
|
||||||
@@ -247,7 +310,7 @@ impl JsTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rt.spawn(async move {
|
rt.spawn(async move {
|
||||||
let stats = table.compact_files(options).await;
|
let stats = table.compact_files(options, None).await;
|
||||||
|
|
||||||
deferred.settle_with(&channel, move |mut cx| {
|
deferred.settle_with(&channel, move |mut cx| {
|
||||||
let stats = stats.or_throw(&mut cx)?;
|
let stats = stats.or_throw(&mut cx)?;
|
||||||
@@ -276,4 +339,91 @@ impl JsTable {
|
|||||||
});
|
});
|
||||||
Ok(promise)
|
Ok(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn js_list_indices(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||||
|
let js_table = cx.this().downcast_or_throw::<JsBox<JsTable>, _>(&mut cx)?;
|
||||||
|
let rt = runtime(&mut cx)?;
|
||||||
|
let (deferred, promise) = cx.promise();
|
||||||
|
// let predicate = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||||
|
let channel = cx.channel();
|
||||||
|
let table = js_table.table.clone();
|
||||||
|
|
||||||
|
rt.spawn(async move {
|
||||||
|
let indices = table.load_indices().await;
|
||||||
|
|
||||||
|
deferred.settle_with(&channel, move |mut cx| {
|
||||||
|
let indices = indices.or_throw(&mut cx)?;
|
||||||
|
|
||||||
|
let output = JsArray::new(&mut cx, indices.len() as u32);
|
||||||
|
for (i, index) in indices.iter().enumerate() {
|
||||||
|
let js_index = JsObject::new(&mut cx);
|
||||||
|
let index_name = cx.string(index.index_name.clone());
|
||||||
|
js_index.set(&mut cx, "name", index_name)?;
|
||||||
|
|
||||||
|
let index_uuid = cx.string(index.index_uuid.clone());
|
||||||
|
js_index.set(&mut cx, "uuid", index_uuid)?;
|
||||||
|
|
||||||
|
let js_index_columns = JsArray::new(&mut cx, index.columns.len() as u32);
|
||||||
|
for (j, column) in index.columns.iter().enumerate() {
|
||||||
|
let js_column = cx.string(column.clone());
|
||||||
|
js_index_columns.set(&mut cx, j as u32, js_column)?;
|
||||||
|
}
|
||||||
|
js_index.set(&mut cx, "columns", js_index_columns)?;
|
||||||
|
|
||||||
|
output.set(&mut cx, i as u32, js_index)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Ok(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn js_index_stats(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||||
|
let js_table = cx.this().downcast_or_throw::<JsBox<JsTable>, _>(&mut cx)?;
|
||||||
|
let rt = runtime(&mut cx)?;
|
||||||
|
let (deferred, promise) = cx.promise();
|
||||||
|
let index_uuid = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||||
|
let channel = cx.channel();
|
||||||
|
let table = js_table.table.clone();
|
||||||
|
|
||||||
|
rt.spawn(async move {
|
||||||
|
let load_stats = futures::try_join!(
|
||||||
|
table.count_indexed_rows(&index_uuid),
|
||||||
|
table.count_unindexed_rows(&index_uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
deferred.settle_with(&channel, move |mut cx| {
|
||||||
|
let (indexed_rows, unindexed_rows) = load_stats.or_throw(&mut cx)?;
|
||||||
|
|
||||||
|
let output = JsObject::new(&mut cx);
|
||||||
|
|
||||||
|
match indexed_rows {
|
||||||
|
Some(x) => {
|
||||||
|
let i = cx.number(x as f64);
|
||||||
|
output.set(&mut cx, "numIndexedRows", i)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let null = cx.null();
|
||||||
|
output.set(&mut cx, "numIndexedRows", null)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match unindexed_rows {
|
||||||
|
Some(x) => {
|
||||||
|
let i = cx.number(x as f64);
|
||||||
|
output.set(&mut cx, "numUnindexedRows", i)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let null = cx.null();
|
||||||
|
output.set(&mut cx, "numUnindexedRows", null)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(promise)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vectordb"
|
name = "vectordb"
|
||||||
version = "0.3.3"
|
version = "0.3.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "LanceDB: A serverless, low-latency vector database for AI applications"
|
description = "LanceDB: A serverless, low-latency vector database for AI applications"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
@@ -21,6 +21,7 @@ object_store = { workspace = true }
|
|||||||
snafu = { workspace = true }
|
snafu = { workspace = true }
|
||||||
half = { workspace = true }
|
half = { workspace = true }
|
||||||
lance = { workspace = true }
|
lance = { workspace = true }
|
||||||
|
lance-index = { workspace = true }
|
||||||
lance-linalg = { workspace = true }
|
lance-linalg = { workspace = true }
|
||||||
lance-testing = { workspace = true }
|
lance-testing = { workspace = true }
|
||||||
tokio = { version = "1.23", features = ["rt-multi-thread"] }
|
tokio = { version = "1.23", features = ["rt-multi-thread"] }
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ impl Database {
|
|||||||
///
|
///
|
||||||
/// * A [Vec<String>] with all table names.
|
/// * A [Vec<String>] with all table names.
|
||||||
pub async fn table_names(&self) -> Result<Vec<String>> {
|
pub async fn table_names(&self) -> Result<Vec<String>> {
|
||||||
let f = self
|
let mut f = self
|
||||||
.object_store
|
.object_store
|
||||||
.read_dir(self.base_path.clone())
|
.read_dir(self.base_path.clone())
|
||||||
.await?
|
.await?
|
||||||
@@ -175,7 +175,8 @@ impl Database {
|
|||||||
is_lance.unwrap_or(false)
|
is_lance.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.filter_map(|p| p.file_stem().and_then(|s| s.to_str().map(String::from)))
|
.filter_map(|p| p.file_stem().and_then(|s| s.to_str().map(String::from)))
|
||||||
.collect();
|
.collect::<Vec<String>>();
|
||||||
|
f.sort();
|
||||||
Ok(f)
|
Ok(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +313,8 @@ mod tests {
|
|||||||
let db = Database::connect(uri).await.unwrap();
|
let db = Database::connect(uri).await.unwrap();
|
||||||
let tables = db.table_names().await.unwrap();
|
let tables = db.table_names().await.unwrap();
|
||||||
assert_eq!(tables.len(), 2);
|
assert_eq!(tables.len(), 2);
|
||||||
assert!(tables.contains(&String::from("table1")));
|
assert!(tables[0].eq(&String::from("table1")));
|
||||||
assert!(tables.contains(&String::from("table2")));
|
assert!(tables[1].eq(&String::from("table2")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use lance::index::vector::ivf::IvfBuildParams;
|
use lance::format::{Index, Manifest};
|
||||||
use lance::index::vector::pq::PQBuildParams;
|
use lance::index::vector::pq::PQBuildParams;
|
||||||
use lance::index::vector::VectorIndexParams;
|
use lance::index::vector::VectorIndexParams;
|
||||||
|
use lance_index::vector::ivf::IvfBuildParams;
|
||||||
use lance_linalg::distance::MetricType;
|
use lance_linalg::distance::MetricType;
|
||||||
|
|
||||||
pub trait VectorIndexBuilder {
|
pub trait VectorIndexBuilder {
|
||||||
@@ -98,7 +99,11 @@ impl VectorIndexBuilder for IvfPQIndexBuilder {
|
|||||||
let ivf_params = self.ivf_params.clone().unwrap_or_default();
|
let ivf_params = self.ivf_params.clone().unwrap_or_default();
|
||||||
let pq_params = self.pq_params.clone().unwrap_or_default();
|
let pq_params = self.pq_params.clone().unwrap_or_default();
|
||||||
|
|
||||||
VectorIndexParams::with_ivf_pq_params(pq_params.metric_type, ivf_params, pq_params)
|
VectorIndexParams::with_ivf_pq_params(
|
||||||
|
self.metric_type.unwrap_or(MetricType::L2),
|
||||||
|
ivf_params,
|
||||||
|
pq_params,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_replace(&self) -> bool {
|
fn get_replace(&self) -> bool {
|
||||||
@@ -106,13 +111,34 @@ impl VectorIndexBuilder for IvfPQIndexBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct VectorIndex {
|
||||||
|
pub columns: Vec<String>,
|
||||||
|
pub index_name: String,
|
||||||
|
pub index_uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VectorIndex {
|
||||||
|
pub fn new_from_format(manifest: &Manifest, index: &Index) -> VectorIndex {
|
||||||
|
let fields = index
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|i| manifest.schema.fields[*i as usize].name.clone())
|
||||||
|
.collect();
|
||||||
|
VectorIndex {
|
||||||
|
columns: fields,
|
||||||
|
index_name: index.name.clone(),
|
||||||
|
index_uuid: index.uuid.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use lance::index::vector::ivf::IvfBuildParams;
|
|
||||||
use lance::index::vector::pq::PQBuildParams;
|
|
||||||
use lance::index::vector::StageParams;
|
use lance::index::vector::StageParams;
|
||||||
|
use lance_index::vector::ivf::IvfBuildParams;
|
||||||
|
use lance_index::vector::pq::PQBuildParams;
|
||||||
|
|
||||||
use crate::index::vector::{IvfPQIndexBuilder, VectorIndexBuilder};
|
use crate::index::vector::{IvfPQIndexBuilder, VectorIndexBuilder};
|
||||||
|
|
||||||
@@ -158,7 +184,6 @@ mod tests {
|
|||||||
pq_params.max_iters = 1;
|
pq_params.max_iters = 1;
|
||||||
pq_params.num_bits = 8;
|
pq_params.num_bits = 8;
|
||||||
pq_params.num_sub_vectors = 50;
|
pq_params.num_sub_vectors = 50;
|
||||||
pq_params.metric_type = MetricType::Cosine;
|
|
||||||
pq_params.max_opq_iters = 2;
|
pq_params.max_opq_iters = 2;
|
||||||
index_builder.ivf_params(ivf_params);
|
index_builder.ivf_params(ivf_params);
|
||||||
index_builder.pq_params(pq_params);
|
index_builder.pq_params(pq_params);
|
||||||
@@ -176,7 +201,6 @@ mod tests {
|
|||||||
assert_eq!(pq_params.max_iters, 1);
|
assert_eq!(pq_params.max_iters, 1);
|
||||||
assert_eq!(pq_params.num_bits, 8);
|
assert_eq!(pq_params.num_bits, 8);
|
||||||
assert_eq!(pq_params.num_sub_vectors, 50);
|
assert_eq!(pq_params.num_sub_vectors, 50);
|
||||||
assert_eq!(pq_params.metric_type, MetricType::Cosine);
|
|
||||||
assert_eq!(pq_params.max_opq_iters, 2);
|
assert_eq!(pq_params.max_opq_iters, 2);
|
||||||
} else {
|
} else {
|
||||||
assert!(false, "Expected second stage to be pq")
|
assert!(false, "Expected second stage to be pq")
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ use bytes::Bytes;
|
|||||||
use futures::{stream::BoxStream, FutureExt, StreamExt};
|
use futures::{stream::BoxStream, FutureExt, StreamExt};
|
||||||
use lance::io::object_store::WrappingObjectStore;
|
use lance::io::object_store::WrappingObjectStore;
|
||||||
use object_store::{
|
use object_store::{
|
||||||
path::Path, GetOptions, GetResult, ListResult, MultipartId, ObjectMeta, ObjectStore, Result,
|
path::Path, Error, GetOptions, GetResult, ListResult, MultipartId, ObjectMeta, ObjectStore,
|
||||||
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -120,7 +121,10 @@ impl ObjectStore for MirroringObjectStore {
|
|||||||
|
|
||||||
async fn delete(&self, location: &Path) -> Result<()> {
|
async fn delete(&self, location: &Path) -> Result<()> {
|
||||||
if !location.primary_only() {
|
if !location.primary_only() {
|
||||||
self.secondary.delete(location).await?;
|
match self.secondary.delete(location).await {
|
||||||
|
Err(Error::NotFound { .. }) | Ok(_) => {}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.primary.delete(location).await
|
self.primary.delete(location).await
|
||||||
}
|
}
|
||||||
@@ -355,7 +359,7 @@ mod test {
|
|||||||
assert_eq!(t.count_rows().await.unwrap(), 100);
|
assert_eq!(t.count_rows().await.unwrap(), 100);
|
||||||
|
|
||||||
let q = t
|
let q = t
|
||||||
.search(PrimitiveArray::from_iter_values(vec![0.1, 0.1, 0.1, 0.1]))
|
.search(Some(PrimitiveArray::from_iter_values(vec![0.1, 0.1, 0.1, 0.1])))
|
||||||
.limit(10)
|
.limit(10)
|
||||||
.execute()
|
.execute()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -24,14 +24,15 @@ use crate::error::Result;
|
|||||||
/// A builder for nearest neighbor queries for LanceDB.
|
/// A builder for nearest neighbor queries for LanceDB.
|
||||||
pub struct Query {
|
pub struct Query {
|
||||||
pub dataset: Arc<Dataset>,
|
pub dataset: Arc<Dataset>,
|
||||||
pub query_vector: Float32Array,
|
pub query_vector: Option<Float32Array>,
|
||||||
pub limit: usize,
|
pub limit: Option<usize>,
|
||||||
pub filter: Option<String>,
|
pub filter: Option<String>,
|
||||||
pub select: Option<Vec<String>>,
|
pub select: Option<Vec<String>>,
|
||||||
pub nprobes: usize,
|
pub nprobes: usize,
|
||||||
pub refine_factor: Option<u32>,
|
pub refine_factor: Option<u32>,
|
||||||
pub metric_type: Option<MetricType>,
|
pub metric_type: Option<MetricType>,
|
||||||
pub use_index: bool,
|
pub use_index: bool,
|
||||||
|
pub prefilter: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Query {
|
impl Query {
|
||||||
@@ -45,17 +46,18 @@ impl Query {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// * A [Query] object.
|
/// * A [Query] object.
|
||||||
pub(crate) fn new(dataset: Arc<Dataset>, vector: Float32Array) -> Self {
|
pub(crate) fn new(dataset: Arc<Dataset>, vector: Option<Float32Array>) -> Self {
|
||||||
Query {
|
Query {
|
||||||
dataset,
|
dataset,
|
||||||
query_vector: vector,
|
query_vector: vector,
|
||||||
limit: 10,
|
limit: None,
|
||||||
nprobes: 20,
|
nprobes: 20,
|
||||||
refine_factor: None,
|
refine_factor: None,
|
||||||
metric_type: None,
|
metric_type: None,
|
||||||
use_index: true,
|
use_index: true,
|
||||||
filter: None,
|
filter: None,
|
||||||
select: None,
|
select: None,
|
||||||
|
prefilter: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +69,17 @@ impl Query {
|
|||||||
pub async fn execute(&self) -> Result<DatasetRecordBatchStream> {
|
pub async fn execute(&self) -> Result<DatasetRecordBatchStream> {
|
||||||
let mut scanner: Scanner = self.dataset.scan();
|
let mut scanner: Scanner = self.dataset.scan();
|
||||||
|
|
||||||
scanner.nearest(
|
if let Some(query) = self.query_vector.as_ref() {
|
||||||
crate::table::VECTOR_COLUMN_NAME,
|
// If there is a vector query, default to limit=10 if unspecified
|
||||||
&self.query_vector,
|
scanner.nearest(crate::table::VECTOR_COLUMN_NAME, query, self.limit.unwrap_or(10))?;
|
||||||
self.limit,
|
} else {
|
||||||
)?;
|
// If there is no vector query, it's ok to not have a limit
|
||||||
|
scanner.limit(self.limit.map(|limit| limit as i64), None)?;
|
||||||
|
}
|
||||||
scanner.nprobs(self.nprobes);
|
scanner.nprobs(self.nprobes);
|
||||||
scanner.use_index(self.use_index);
|
scanner.use_index(self.use_index);
|
||||||
|
scanner.prefilter(self.prefilter);
|
||||||
|
|
||||||
self.select.as_ref().map(|p| scanner.project(p.as_slice()));
|
self.select.as_ref().map(|p| scanner.project(p.as_slice()));
|
||||||
self.filter.as_ref().map(|f| scanner.filter(f));
|
self.filter.as_ref().map(|f| scanner.filter(f));
|
||||||
self.refine_factor.map(|rf| scanner.refine(rf));
|
self.refine_factor.map(|rf| scanner.refine(rf));
|
||||||
@@ -87,7 +93,7 @@ impl Query {
|
|||||||
///
|
///
|
||||||
/// * `limit` - The maximum number of results to return.
|
/// * `limit` - The maximum number of results to return.
|
||||||
pub fn limit(mut self, limit: usize) -> Query {
|
pub fn limit(mut self, limit: usize) -> Query {
|
||||||
self.limit = limit;
|
self.limit = Some(limit);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +103,7 @@ impl Query {
|
|||||||
///
|
///
|
||||||
/// * `vector` - The vector that will be used for search.
|
/// * `vector` - The vector that will be used for search.
|
||||||
pub fn query_vector(mut self, query_vector: Float32Array) -> Query {
|
pub fn query_vector(mut self, query_vector: Float32Array) -> Query {
|
||||||
self.query_vector = query_vector;
|
self.query_vector = Some(query_vector);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +164,11 @@ impl Query {
|
|||||||
self.select = columns;
|
self.select = columns;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prefilter(mut self, prefilter: bool) -> Query {
|
||||||
|
self.prefilter = prefilter;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -165,9 +176,11 @@ mod tests {
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use arrow_array::{Float32Array, RecordBatch, RecordBatchIterator, RecordBatchReader};
|
use arrow_array::{Float32Array, RecordBatch, RecordBatchIterator, RecordBatchReader, cast::AsArray, Int32Array};
|
||||||
use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
|
use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
|
||||||
|
use futures::StreamExt;
|
||||||
use lance::dataset::Dataset;
|
use lance::dataset::Dataset;
|
||||||
|
use lance_testing::datagen::{BatchGenerator, IncrementingInt32, RandomVector};
|
||||||
|
|
||||||
use crate::query::Query;
|
use crate::query::Query;
|
||||||
|
|
||||||
@@ -176,7 +189,7 @@ mod tests {
|
|||||||
let batches = make_test_batches();
|
let batches = make_test_batches();
|
||||||
let ds = Dataset::write(batches, "memory://foo", None).await.unwrap();
|
let ds = Dataset::write(batches, "memory://foo", None).await.unwrap();
|
||||||
|
|
||||||
let vector = Float32Array::from_iter_values([0.1, 0.2]);
|
let vector = Some(Float32Array::from_iter_values([0.1, 0.2]));
|
||||||
let query = Query::new(Arc::new(ds), vector.clone());
|
let query = Query::new(Arc::new(ds), vector.clone());
|
||||||
assert_eq!(query.query_vector, vector);
|
assert_eq!(query.query_vector, vector);
|
||||||
|
|
||||||
@@ -190,8 +203,8 @@ mod tests {
|
|||||||
.metric_type(Some(MetricType::Cosine))
|
.metric_type(Some(MetricType::Cosine))
|
||||||
.refine_factor(Some(999));
|
.refine_factor(Some(999));
|
||||||
|
|
||||||
assert_eq!(query.query_vector, new_vector);
|
assert_eq!(query.query_vector.unwrap(), new_vector);
|
||||||
assert_eq!(query.limit, 100);
|
assert_eq!(query.limit.unwrap(), 100);
|
||||||
assert_eq!(query.nprobes, 1000);
|
assert_eq!(query.nprobes, 1000);
|
||||||
assert_eq!(query.use_index, true);
|
assert_eq!(query.use_index, true);
|
||||||
assert_eq!(query.metric_type, Some(MetricType::Cosine));
|
assert_eq!(query.metric_type, Some(MetricType::Cosine));
|
||||||
@@ -200,13 +213,64 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_execute() {
|
async fn test_execute() {
|
||||||
let batches = make_test_batches();
|
let batches = make_non_empty_batches();
|
||||||
let ds = Dataset::write(batches, "memory://foo", None).await.unwrap();
|
let ds = Arc::new(Dataset::write(batches, "memory://foo", None).await.unwrap());
|
||||||
|
|
||||||
let vector = Float32Array::from_iter_values([0.1; 128]);
|
let vector = Some(Float32Array::from_iter_values([0.1; 4]));
|
||||||
let query = Query::new(Arc::new(ds), vector.clone());
|
|
||||||
let result = query.execute().await;
|
let query = Query::new(ds.clone(), vector.clone());
|
||||||
assert_eq!(result.is_ok(), true);
|
let result = query
|
||||||
|
.limit(10)
|
||||||
|
.filter(Some("id % 2 == 0".to_string()))
|
||||||
|
.execute()
|
||||||
|
.await;
|
||||||
|
let mut stream = result.expect("should have result");
|
||||||
|
// should only have one batch
|
||||||
|
while let Some(batch) = stream.next().await {
|
||||||
|
// post filter should have removed some rows
|
||||||
|
assert!(batch.expect("should be Ok").num_rows() < 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = Query::new(ds, vector.clone());
|
||||||
|
let result = query
|
||||||
|
.limit(10)
|
||||||
|
.filter(Some("id % 2 == 0".to_string()))
|
||||||
|
.prefilter(true)
|
||||||
|
.execute()
|
||||||
|
.await;
|
||||||
|
let mut stream = result.expect("should have result");
|
||||||
|
// should only have one batch
|
||||||
|
while let Some(batch) = stream.next().await {
|
||||||
|
// pre filter should return 10 rows
|
||||||
|
assert!(batch.expect("should be Ok").num_rows() == 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_no_vector() {
|
||||||
|
// test that it's ok to not specify a query vector (just filter / limit)
|
||||||
|
let batches = make_non_empty_batches();
|
||||||
|
let ds = Arc::new(Dataset::write(batches, "memory://foo", None).await.unwrap());
|
||||||
|
|
||||||
|
let query = Query::new(ds.clone(), None);
|
||||||
|
let result = query
|
||||||
|
.filter(Some("id % 2 == 0".to_string()))
|
||||||
|
.execute()
|
||||||
|
.await;
|
||||||
|
let mut stream = result.expect("should have result");
|
||||||
|
// should only have one batch
|
||||||
|
while let Some(batch) = stream.next().await {
|
||||||
|
let b = batch.expect("should be Ok");
|
||||||
|
// cast arr into Int32Array
|
||||||
|
let arr: &Int32Array = b["id"].as_primitive();
|
||||||
|
assert!(arr.iter().all(|x| x.unwrap() % 2 == 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_non_empty_batches() -> impl RecordBatchReader + Send + 'static {
|
||||||
|
let vec = Box::new(RandomVector::new().named("vector".to_string()));
|
||||||
|
let id = Box::new(IncrementingInt32::new().named("id".to_string()));
|
||||||
|
BatchGenerator::new().col(vec).col(id).batch(512)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_test_batches() -> impl RecordBatchReader + Send + 'static {
|
fn make_test_batches() -> impl RecordBatchReader + Send + 'static {
|
||||||
|
|||||||
@@ -13,19 +13,23 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use lance::dataset::builder::DatasetBuilder;
|
||||||
|
use lance_index::IndexType;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use arrow_array::{Float32Array, RecordBatchReader};
|
use arrow_array::{Float32Array, RecordBatchReader};
|
||||||
use arrow_schema::SchemaRef;
|
use arrow_schema::SchemaRef;
|
||||||
use lance::dataset::cleanup::RemovalStats;
|
use lance::dataset::cleanup::RemovalStats;
|
||||||
use lance::dataset::optimize::{compact_files, CompactionMetrics, CompactionOptions};
|
use lance::dataset::optimize::{
|
||||||
use lance::dataset::{Dataset, WriteParams};
|
compact_files, CompactionMetrics, CompactionOptions, IndexRemapperOptions,
|
||||||
use lance::index::IndexType;
|
};
|
||||||
|
use lance::dataset::{Dataset, UpdateBuilder, WriteParams};
|
||||||
|
use lance::index::DatasetIndexExt;
|
||||||
use lance::io::object_store::WrappingObjectStore;
|
use lance::io::object_store::WrappingObjectStore;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::index::vector::VectorIndexBuilder;
|
use crate::index::vector::{VectorIndex, VectorIndexBuilder};
|
||||||
use crate::query::Query;
|
use crate::query::Query;
|
||||||
use crate::utils::{PatchReadParam, PatchWriteParam};
|
use crate::utils::{PatchReadParam, PatchWriteParam};
|
||||||
use crate::WriteMode;
|
use crate::WriteMode;
|
||||||
@@ -94,7 +98,10 @@ impl Table {
|
|||||||
Some(wrapper) => params.patch_with_store_wrapper(wrapper)?,
|
Some(wrapper) => params.patch_with_store_wrapper(wrapper)?,
|
||||||
None => params,
|
None => params,
|
||||||
};
|
};
|
||||||
let dataset = Dataset::open_with_params(uri, ¶ms)
|
|
||||||
|
let dataset = DatasetBuilder::from_uri(uri)
|
||||||
|
.with_read_params(params)
|
||||||
|
.load()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
lance::Error::DatasetNotFound { .. } => Error::TableNotFound {
|
lance::Error::DatasetNotFound { .. } => Error::TableNotFound {
|
||||||
@@ -238,8 +245,6 @@ impl Table {
|
|||||||
|
|
||||||
/// Create index on the table.
|
/// Create index on the table.
|
||||||
pub async fn create_index(&mut self, index_builder: &impl VectorIndexBuilder) -> Result<()> {
|
pub async fn create_index(&mut self, index_builder: &impl VectorIndexBuilder) -> Result<()> {
|
||||||
use lance::index::DatasetIndexExt;
|
|
||||||
|
|
||||||
let mut dataset = self.dataset.as_ref().clone();
|
let mut dataset = self.dataset.as_ref().clone();
|
||||||
dataset
|
dataset
|
||||||
.create_index(
|
.create_index(
|
||||||
@@ -257,6 +262,14 @@ impl Table {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn optimize_indices(&mut self) -> Result<()> {
|
||||||
|
let mut dataset = self.dataset.as_ref().clone();
|
||||||
|
|
||||||
|
dataset.optimize_indices().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert records into this Table
|
/// Insert records into this Table
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -295,10 +308,14 @@ impl Table {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// * A [Query] object.
|
/// * A [Query] object.
|
||||||
pub fn search(&self, query_vector: Float32Array) -> Query {
|
pub fn search(&self, query_vector: Option<Float32Array>) -> Query {
|
||||||
Query::new(self.dataset.clone(), query_vector)
|
Query::new(self.dataset.clone(), query_vector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filter(&self, expr: String) -> Query {
|
||||||
|
Query::new(self.dataset.clone(), None).filter(Some(expr))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the number of rows in this Table
|
/// Returns the number of rows in this Table
|
||||||
pub async fn count_rows(&self) -> Result<usize> {
|
pub async fn count_rows(&self) -> Result<usize> {
|
||||||
Ok(self.dataset.count_rows().await?)
|
Ok(self.dataset.count_rows().await?)
|
||||||
@@ -325,6 +342,27 @@ impl Table {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update(
|
||||||
|
&mut self,
|
||||||
|
predicate: Option<&str>,
|
||||||
|
updates: Vec<(&str, &str)>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut builder = UpdateBuilder::new(self.dataset.clone());
|
||||||
|
if let Some(predicate) = predicate {
|
||||||
|
builder = builder.update_where(predicate)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (column, value) in updates {
|
||||||
|
builder = builder.set(column, value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let operation = builder.build()?;
|
||||||
|
let new_ds = operation.execute().await?;
|
||||||
|
self.dataset = new_ds;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove old versions of the dataset from disk.
|
/// Remove old versions of the dataset from disk.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -353,12 +391,45 @@ impl Table {
|
|||||||
/// for faster reads.
|
/// for faster reads.
|
||||||
///
|
///
|
||||||
/// This calls into [lance::dataset::optimize::compact_files].
|
/// This calls into [lance::dataset::optimize::compact_files].
|
||||||
pub async fn compact_files(&mut self, options: CompactionOptions) -> Result<CompactionMetrics> {
|
pub async fn compact_files(
|
||||||
|
&mut self,
|
||||||
|
options: CompactionOptions,
|
||||||
|
remap_options: Option<Arc<dyn IndexRemapperOptions>>,
|
||||||
|
) -> Result<CompactionMetrics> {
|
||||||
let mut dataset = self.dataset.as_ref().clone();
|
let mut dataset = self.dataset.as_ref().clone();
|
||||||
let metrics = compact_files(&mut dataset, options, None).await?;
|
let metrics = compact_files(&mut dataset, options, remap_options).await?;
|
||||||
self.dataset = Arc::new(dataset);
|
self.dataset = Arc::new(dataset);
|
||||||
Ok(metrics)
|
Ok(metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn count_fragments(&self) -> usize {
|
||||||
|
self.dataset.count_fragments()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_deleted_rows(&self) -> Result<usize> {
|
||||||
|
Ok(self.dataset.count_deleted_rows().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn num_small_files(&self, max_rows_per_group: usize) -> usize {
|
||||||
|
self.dataset.num_small_files(max_rows_per_group).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_indexed_rows(&self, index_uuid: &str) -> Result<Option<usize>> {
|
||||||
|
Ok(self.dataset.count_indexed_rows(index_uuid).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_unindexed_rows(&self, index_uuid: &str) -> Result<Option<usize>> {
|
||||||
|
Ok(self.dataset.count_unindexed_rows(index_uuid).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_indices(&self) -> Result<Vec<VectorIndex>> {
|
||||||
|
let (indices, mf) =
|
||||||
|
futures::try_join!(self.dataset.load_indices(), self.dataset.latest_manifest())?;
|
||||||
|
Ok(indices
|
||||||
|
.iter()
|
||||||
|
.map(|i| VectorIndex::new_from_format(&mf, i))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -367,15 +438,18 @@ mod tests {
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use arrow_array::{
|
use arrow_array::{
|
||||||
Array, FixedSizeListArray, Float32Array, Int32Array, RecordBatch, RecordBatchIterator,
|
Array, BooleanArray, Date32Array, FixedSizeListArray, Float32Array, Float64Array,
|
||||||
RecordBatchReader,
|
Int32Array, Int64Array, LargeStringArray, RecordBatch, RecordBatchIterator,
|
||||||
|
RecordBatchReader, StringArray, TimestampMillisecondArray, TimestampNanosecondArray,
|
||||||
|
UInt32Array,
|
||||||
};
|
};
|
||||||
use arrow_data::ArrayDataBuilder;
|
use arrow_data::ArrayDataBuilder;
|
||||||
use arrow_schema::{DataType, Field, Schema};
|
use arrow_schema::{DataType, Field, Schema, TimeUnit};
|
||||||
|
use futures::TryStreamExt;
|
||||||
use lance::dataset::{Dataset, WriteMode};
|
use lance::dataset::{Dataset, WriteMode};
|
||||||
use lance::index::vector::ivf::IvfBuildParams;
|
|
||||||
use lance::index::vector::pq::PQBuildParams;
|
use lance::index::vector::pq::PQBuildParams;
|
||||||
use lance::io::object_store::{ObjectStoreParams, WrappingObjectStore};
|
use lance::io::object_store::{ObjectStoreParams, WrappingObjectStore};
|
||||||
|
use lance_index::vector::ivf::IvfBuildParams;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
@@ -494,6 +568,272 @@ mod tests {
|
|||||||
assert_eq!(table.name, "test");
|
assert_eq!(table.name, "test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_with_predicate() {
|
||||||
|
let tmp_dir = tempdir().unwrap();
|
||||||
|
let dataset_path = tmp_dir.path().join("test.lance");
|
||||||
|
let uri = dataset_path.to_str().unwrap();
|
||||||
|
|
||||||
|
let schema = Arc::new(Schema::new(vec![
|
||||||
|
Field::new("id", DataType::Int32, false),
|
||||||
|
Field::new("name", DataType::Utf8, false),
|
||||||
|
]));
|
||||||
|
|
||||||
|
let record_batch_iter = RecordBatchIterator::new(
|
||||||
|
vec![RecordBatch::try_new(
|
||||||
|
schema.clone(),
|
||||||
|
vec![
|
||||||
|
Arc::new(Int32Array::from_iter_values(0..10)),
|
||||||
|
Arc::new(StringArray::from_iter_values(vec![
|
||||||
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||||
|
])),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap()]
|
||||||
|
.into_iter()
|
||||||
|
.map(Ok),
|
||||||
|
schema.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Dataset::write(record_batch_iter, uri, None).await.unwrap();
|
||||||
|
let mut table = Table::open(uri).await.unwrap();
|
||||||
|
|
||||||
|
table
|
||||||
|
.update(Some("id > 5"), vec![("name", "'foo'")])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ds_after = Dataset::open(uri).await.unwrap();
|
||||||
|
let mut batches = ds_after
|
||||||
|
.scan()
|
||||||
|
.project(&["id", "name"])
|
||||||
|
.unwrap()
|
||||||
|
.try_into_stream()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
while let Some(batch) = batches.pop() {
|
||||||
|
let ids = batch
|
||||||
|
.column(0)
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<Int32Array>()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let names = batch
|
||||||
|
.column(1)
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<StringArray>()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
let id = ids[i].unwrap();
|
||||||
|
let name = name.unwrap();
|
||||||
|
if id > 5 {
|
||||||
|
assert_eq!(name, "foo");
|
||||||
|
} else {
|
||||||
|
assert_eq!(name, &format!("{}", (b'a' + id as u8) as char));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_all_types() {
|
||||||
|
let tmp_dir = tempdir().unwrap();
|
||||||
|
let dataset_path = tmp_dir.path().join("test.lance");
|
||||||
|
let uri = dataset_path.to_str().unwrap();
|
||||||
|
|
||||||
|
let schema = Arc::new(Schema::new(vec![
|
||||||
|
Field::new("int32", DataType::Int32, false),
|
||||||
|
Field::new("int64", DataType::Int64, false),
|
||||||
|
Field::new("uint32", DataType::UInt32, false),
|
||||||
|
Field::new("string", DataType::Utf8, false),
|
||||||
|
Field::new("large_string", DataType::LargeUtf8, false),
|
||||||
|
Field::new("float32", DataType::Float32, false),
|
||||||
|
Field::new("float64", DataType::Float64, false),
|
||||||
|
Field::new("bool", DataType::Boolean, false),
|
||||||
|
Field::new("date32", DataType::Date32, false),
|
||||||
|
Field::new(
|
||||||
|
"timestamp_ns",
|
||||||
|
DataType::Timestamp(TimeUnit::Nanosecond, None),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Field::new(
|
||||||
|
"timestamp_ms",
|
||||||
|
DataType::Timestamp(TimeUnit::Millisecond, None),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Field::new(
|
||||||
|
"vec_f32",
|
||||||
|
DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), 2),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Field::new(
|
||||||
|
"vec_f64",
|
||||||
|
DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float64, true)), 2),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
|
||||||
|
let record_batch_iter = RecordBatchIterator::new(
|
||||||
|
vec![RecordBatch::try_new(
|
||||||
|
schema.clone(),
|
||||||
|
vec![
|
||||||
|
Arc::new(Int32Array::from_iter_values(0..10)),
|
||||||
|
Arc::new(Int64Array::from_iter_values(0..10)),
|
||||||
|
Arc::new(UInt32Array::from_iter_values(0..10)),
|
||||||
|
Arc::new(StringArray::from_iter_values(vec![
|
||||||
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||||
|
])),
|
||||||
|
Arc::new(LargeStringArray::from_iter_values(vec![
|
||||||
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||||
|
])),
|
||||||
|
Arc::new(Float32Array::from_iter_values(
|
||||||
|
(0..10).into_iter().map(|i| i as f32),
|
||||||
|
)),
|
||||||
|
Arc::new(Float64Array::from_iter_values(
|
||||||
|
(0..10).into_iter().map(|i| i as f64),
|
||||||
|
)),
|
||||||
|
Arc::new(Into::<BooleanArray>::into(vec![
|
||||||
|
true, false, true, false, true, false, true, false, true, false,
|
||||||
|
])),
|
||||||
|
Arc::new(Date32Array::from_iter_values(0..10)),
|
||||||
|
Arc::new(TimestampNanosecondArray::from_iter_values(0..10)),
|
||||||
|
Arc::new(TimestampMillisecondArray::from_iter_values(0..10)),
|
||||||
|
Arc::new(
|
||||||
|
create_fixed_size_list(
|
||||||
|
Float32Array::from_iter_values((0..20).into_iter().map(|i| i as f32)),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
Arc::new(
|
||||||
|
create_fixed_size_list(
|
||||||
|
Float64Array::from_iter_values((0..20).into_iter().map(|i| i as f64)),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap()]
|
||||||
|
.into_iter()
|
||||||
|
.map(Ok),
|
||||||
|
schema.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Dataset::write(record_batch_iter, uri, None).await.unwrap();
|
||||||
|
let mut table = Table::open(uri).await.unwrap();
|
||||||
|
|
||||||
|
// check it can do update for each type
|
||||||
|
let updates: Vec<(&str, &str)> = vec![
|
||||||
|
("string", "'foo'"),
|
||||||
|
("large_string", "'large_foo'"),
|
||||||
|
("int32", "1"),
|
||||||
|
("int64", "1"),
|
||||||
|
("uint32", "1"),
|
||||||
|
("float32", "1.0"),
|
||||||
|
("float64", "1.0"),
|
||||||
|
("bool", "true"),
|
||||||
|
("date32", "1"),
|
||||||
|
("timestamp_ns", "1"),
|
||||||
|
("timestamp_ms", "1"),
|
||||||
|
("vec_f32", "[1.0, 1.0]"),
|
||||||
|
("vec_f64", "[1.0, 1.0]"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// for (column, value) in test_cases {
|
||||||
|
table.update(None, updates).await.unwrap();
|
||||||
|
|
||||||
|
let ds_after = Dataset::open(uri).await.unwrap();
|
||||||
|
let mut batches = ds_after
|
||||||
|
.scan()
|
||||||
|
.project(&[
|
||||||
|
"string",
|
||||||
|
"large_string",
|
||||||
|
"int32",
|
||||||
|
"int64",
|
||||||
|
"uint32",
|
||||||
|
"float32",
|
||||||
|
"float64",
|
||||||
|
"bool",
|
||||||
|
"date32",
|
||||||
|
"timestamp_ns",
|
||||||
|
"timestamp_ms",
|
||||||
|
"vec_f32",
|
||||||
|
"vec_f64",
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
.try_into_stream()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let batch = batches.pop().unwrap();
|
||||||
|
|
||||||
|
macro_rules! assert_column {
|
||||||
|
($column:expr, $array_type:ty, $expected:expr) => {
|
||||||
|
let array = $column
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<$array_type>()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for v in array {
|
||||||
|
assert_eq!(v, Some($expected));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_column!(batch.column(0), StringArray, "foo");
|
||||||
|
assert_column!(batch.column(1), LargeStringArray, "large_foo");
|
||||||
|
assert_column!(batch.column(2), Int32Array, 1);
|
||||||
|
assert_column!(batch.column(3), Int64Array, 1);
|
||||||
|
assert_column!(batch.column(4), UInt32Array, 1);
|
||||||
|
assert_column!(batch.column(5), Float32Array, 1.0);
|
||||||
|
assert_column!(batch.column(6), Float64Array, 1.0);
|
||||||
|
assert_column!(batch.column(7), BooleanArray, true);
|
||||||
|
assert_column!(batch.column(8), Date32Array, 1);
|
||||||
|
assert_column!(batch.column(9), TimestampNanosecondArray, 1);
|
||||||
|
assert_column!(batch.column(10), TimestampMillisecondArray, 1);
|
||||||
|
|
||||||
|
let array = batch
|
||||||
|
.column(11)
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<FixedSizeListArray>()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for v in array {
|
||||||
|
let v = v.unwrap();
|
||||||
|
let f32array = v.as_any().downcast_ref::<Float32Array>().unwrap();
|
||||||
|
for v in f32array {
|
||||||
|
assert_eq!(v, Some(1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let array = batch
|
||||||
|
.column(12)
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<FixedSizeListArray>()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for v in array {
|
||||||
|
let v = v.unwrap();
|
||||||
|
let f64array = v.as_any().downcast_ref::<Float64Array>().unwrap();
|
||||||
|
for v in f64array {
|
||||||
|
assert_eq!(v, Some(1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_search() {
|
async fn test_search() {
|
||||||
let tmp_dir = tempdir().unwrap();
|
let tmp_dir = tempdir().unwrap();
|
||||||
@@ -508,8 +848,8 @@ mod tests {
|
|||||||
let table = Table::open(uri).await.unwrap();
|
let table = Table::open(uri).await.unwrap();
|
||||||
|
|
||||||
let vector = Float32Array::from_iter_values([0.1, 0.2]);
|
let vector = Float32Array::from_iter_values([0.1, 0.2]);
|
||||||
let query = table.search(vector.clone());
|
let query = table.search(Some(vector.clone()));
|
||||||
assert_eq!(vector, query.query_vector);
|
assert_eq!(vector, query.query_vector.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
|
|||||||
Reference in New Issue
Block a user