Skip to content

Commit 5ccbd2f

Browse files
committed
feat: Add support for embedded operations in IRISVector and enhance runtime facade with execute, gref, and ref methods
1 parent d537d6b commit 5ccbd2f

7 files changed

Lines changed: 431 additions & 4 deletions

File tree

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,9 @@ For the native object proxy path (`iris.cls(...)` with `iris.runtime` configured
418418
##### ByRef
419419

420420
`iris.ByRef(value)` is a lightweight by-reference container with a mutable
421-
`.value` attribute. `iris.make_ref(value)` returns `iris.ref` when the
422-
embedded runtime exposes it and falls back to `ByRef` otherwise.
421+
`.value` attribute. `iris.ref(value)` and `iris.make_ref(value)` use the
422+
embedded runtime reference type when available and otherwise return a
423+
`ByRef`-compatible container.
423424

424425
For ObjectScript methods that declare a `ByRef` argument:
425426

@@ -478,7 +479,34 @@ with SQL `TO_VECTOR(?, decimal)`, `TO_VECTOR(?, double)`, or
478479
storage; embedded `$VECTOROP` operations evaluate that value as an
479480
ObjectScript double because `$VECTOR` has no float storage type. Vector
480481
operations delegate to embedded ObjectScript `$VECTOROP`, so they require an
481-
embedded runtime with `iris.gref` and `iris.execute` available.
482+
embedded runtime with `iris.gref` and `iris.execute` available. They are not
483+
routed through the remote/native bridge.
484+
485+
FastEmbed is an optional dependency (`pip install fastembed`). It returns NumPy
486+
embedding arrays, which can be passed directly to `iris.Vector`:
487+
488+
```python
489+
from fastembed import TextEmbedding
490+
import iris
491+
492+
model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
493+
embedding = iris.Vector(next(model.embed(["store this text"])), dtype="float")
494+
495+
cur.execute(
496+
"INSERT INTO Demo.Documents (content, embedding) VALUES (?, TO_VECTOR(?, float))",
497+
("store this text", embedding),
498+
)
499+
```
500+
501+
Complete runnable examples:
502+
503+
- [`examples/fastembed_vector_search.py`](examples/fastembed_vector_search.py)
504+
stores embeddings in an IRIS SQL `VECTOR` column and ranks with
505+
`VECTOR_COSINE`.
506+
- [`examples/fastembed_vector_operations.py`](examples/fastembed_vector_operations.py)
507+
keeps the embeddings as Python `iris.Vector` objects and ranks with
508+
`Vector.cosine()` without SQL. It requires embedded `iris.gref` and
509+
`iris.execute`.
482510

483511
#### Connect modes
484512

_iris_ep/_runtime_facade.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from iris_utils._module_exports import copy_public_exports
1010

1111
from . import _bootstrap
12+
from ._byref import ByRef
1213
from ._dbapi import make_dbapi
1314

1415
_WRAPPER_EXPORTS = {
@@ -17,6 +18,9 @@
1718
"dbapi",
1819
"cls",
1920
"connect",
21+
"execute",
22+
"gref",
23+
"ref",
2024
"ByRef",
2125
"make_ref",
2226
"IRISVector",
@@ -287,8 +291,20 @@ def public_cls(class_name):
287291
def public_connect(*args, path=None, **kwargs):
288292
return self.connect(*args, path=path, **kwargs)
289293

294+
def public_execute(statements):
295+
return self.execute(statements)
296+
297+
def public_gref(global_name):
298+
return self.gref(global_name)
299+
300+
def public_ref(value=""):
301+
return self.ref(value)
302+
290303
public_cls.__name__ = "cls"
291304
public_connect.__name__ = "connect"
305+
public_execute.__name__ = "execute"
306+
public_gref.__name__ = "gref"
307+
public_ref.__name__ = "ref"
292308
self._public_cls = public_cls
293309
self._public_connect = public_connect
294310

@@ -297,6 +313,9 @@ def public_connect(*args, path=None, **kwargs):
297313
self.module_globals["dbapi"] = self.dbapi
298314
self.module_globals["cls"] = public_cls
299315
self.module_globals["connect"] = public_connect
316+
self.module_globals["execute"] = public_execute
317+
self.module_globals["gref"] = public_gref
318+
self.module_globals["ref"] = public_ref
300319

301320
def finalize_all(self) -> None:
302321
existing_all = self.module_globals.get("__all__")
@@ -313,6 +332,9 @@ def finalize_all(self) -> None:
313332
"dbapi",
314333
"cls",
315334
"connect",
335+
"execute",
336+
"gref",
337+
"ref",
316338
"ByRef",
317339
"make_ref",
318340
"IRISVector",
@@ -378,6 +400,9 @@ def sync_public_modules(self):
378400
"cls",
379401
"connect",
380402
"system",
403+
"execute",
404+
"gref",
405+
"ref",
381406
"ByRef",
382407
"make_ref",
383408
"IRISVector",
@@ -454,6 +479,82 @@ def get_dbapi_embedded_cls(self):
454479
return cls_candidate
455480
return None
456481

482+
def _get_embedded_module_attr(self, current_runtime, name, *, required=False):
483+
current_runtime = self.ensure_embedded_backend(
484+
current_runtime,
485+
required=required,
486+
)
487+
module = getattr(current_runtime, "embedded_module", None)
488+
if module is not None:
489+
attr = getattr(module, name, None)
490+
if callable(attr):
491+
return attr
492+
return None
493+
494+
def execute(self, statements):
495+
current_runtime = self.runtime_manager.get()
496+
if current_runtime.mode == "native":
497+
if current_runtime.iris is None:
498+
raise RuntimeError("iris.runtime is configured for native mode, but no native IRIS handle is bound")
499+
raise RuntimeError("iris.execute requires an embedded runtime")
500+
501+
if current_runtime.mode == "embedded":
502+
embedded_execute = self._get_embedded_module_attr(
503+
current_runtime,
504+
"execute",
505+
required=True,
506+
)
507+
if callable(embedded_execute):
508+
return embedded_execute(statements)
509+
raise RuntimeError("iris.runtime is configured for embedded mode, but embedded execute is unavailable")
510+
511+
if current_runtime.embedded_available:
512+
embedded_execute = self._get_embedded_module_attr(current_runtime, "execute")
513+
if callable(embedded_execute):
514+
return embedded_execute(statements)
515+
516+
raise RuntimeError("iris.execute requires an embedded runtime")
517+
518+
def gref(self, global_name):
519+
current_runtime = self.runtime_manager.get()
520+
if current_runtime.mode == "native":
521+
if current_runtime.iris is None:
522+
raise RuntimeError("iris.runtime is configured for native mode, but no native IRIS handle is bound")
523+
raise RuntimeError("iris.gref requires an embedded runtime")
524+
525+
if current_runtime.mode == "embedded":
526+
embedded_gref = self._get_embedded_module_attr(
527+
current_runtime,
528+
"gref",
529+
required=True,
530+
)
531+
if callable(embedded_gref):
532+
return embedded_gref(global_name)
533+
raise RuntimeError("iris.runtime is configured for embedded mode, but embedded gref is unavailable")
534+
535+
if current_runtime.embedded_available:
536+
embedded_gref = self._get_embedded_module_attr(current_runtime, "gref")
537+
if callable(embedded_gref):
538+
return embedded_gref(global_name)
539+
540+
raise RuntimeError("iris.gref requires an embedded runtime")
541+
542+
def ref(self, value=""):
543+
current_runtime = self.runtime_manager.get()
544+
if current_runtime.mode == "embedded" or (
545+
current_runtime.mode == "auto" and current_runtime.embedded_available
546+
):
547+
embedded_ref = self._get_embedded_module_attr(
548+
current_runtime,
549+
"ref",
550+
required=current_runtime.mode == "embedded",
551+
)
552+
if callable(embedded_ref):
553+
return embedded_ref(value)
554+
if current_runtime.mode == "embedded":
555+
raise RuntimeError("iris.runtime is configured for embedded mode, but embedded ref is unavailable")
556+
return ByRef(value)
557+
457558
def cls(self, class_name):
458559
current_runtime = self.runtime_manager.get()
459560
if current_runtime.mode == 'native':

_iris_ep/_vector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _coerce_iris_vector_operand(value: Any, dtype: str) -> "IRISVector":
9595

9696

9797
class IRISVector:
98-
"""Python value wrapper for IRIS SQL VECTOR parameters and operations."""
98+
"""Python value wrapper for IRIS SQL VECTOR parameters and embedded operations."""
9999

100100
__slots__ = ("_objectscript_dtype", "_values", "dtype")
101101

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""FastEmbed + iris.Vector operations without SQL.
2+
3+
Prerequisites:
4+
pip install fastembed
5+
6+
Run from an embedded IRIS-capable environment:
7+
python examples/fastembed_vector_operations.py
8+
9+
This example does not create a table or execute SQL. FastEmbed embeddings are
10+
wrapped as iris.Vector objects and ranked with ObjectScript $VECTOROP through
11+
the Python Vector methods. Vector operations require embedded iris.gref and
12+
iris.execute; they are not routed through the remote/native bridge.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
import sys
19+
from collections.abc import Iterable, Sequence
20+
from pathlib import Path
21+
22+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
23+
if str(PROJECT_ROOT) not in sys.path:
24+
sys.path.insert(0, str(PROJECT_ROOT))
25+
26+
import iris
27+
from fastembed import TextEmbedding
28+
29+
30+
DOCUMENTS = [
31+
"IRIS stores vectors in SQL VECTOR columns.",
32+
"FastEmbed generates local text embeddings.",
33+
"ByRef lets Python receive ObjectScript output arguments.",
34+
"VECTOR_COSINE ranks documents by embedding similarity.",
35+
"DB-API parameters should be cast with TO_VECTOR.",
36+
]
37+
38+
39+
def parse_args() -> argparse.Namespace:
40+
parser = argparse.ArgumentParser()
41+
parser.add_argument(
42+
"--model",
43+
default="BAAI/bge-small-en-v1.5",
44+
help="FastEmbed model name.",
45+
)
46+
parser.add_argument(
47+
"--query",
48+
default="How do I store embeddings in IRIS?",
49+
help="Search query to embed and rank against the example documents.",
50+
)
51+
parser.add_argument("--top-k", type=int, default=3)
52+
return parser.parse_args()
53+
54+
55+
def embed_texts(model: TextEmbedding, texts: Iterable[str]) -> list[iris.Vector]:
56+
return [iris.Vector(embedding, dtype="float") for embedding in model.embed(texts)]
57+
58+
59+
def rank_documents(
60+
query_vector: iris.Vector,
61+
documents: Sequence[str],
62+
document_vectors: Sequence[iris.Vector],
63+
top_k: int,
64+
):
65+
scores = [
66+
(query_vector.cosine(document_vector), index, content)
67+
for index, (content, document_vector) in enumerate(
68+
zip(documents, document_vectors),
69+
start=1,
70+
)
71+
]
72+
return sorted(scores, reverse=True)[: max(1, int(top_k))]
73+
74+
75+
def centroid(vectors: Sequence[iris.Vector]) -> iris.Vector:
76+
if not vectors:
77+
raise ValueError("centroid requires at least one vector")
78+
79+
total = vectors[0]
80+
for vector in vectors[1:]:
81+
total = total + vector
82+
return total / len(vectors)
83+
84+
85+
def main() -> None:
86+
args = parse_args()
87+
model = TextEmbedding(model_name=args.model)
88+
89+
document_vectors = embed_texts(model, DOCUMENTS)
90+
query_vector = embed_texts(model, [args.query])[0]
91+
average_document_vector = centroid(document_vectors)
92+
93+
print(f"dimension: {len(query_vector)}")
94+
print(f"query sum: {query_vector.sum():.4f}")
95+
print(f"centroid cosine: {query_vector.cosine(average_document_vector):.4f}")
96+
print()
97+
98+
for score, row_id, content in rank_documents(
99+
query_vector,
100+
DOCUMENTS,
101+
document_vectors,
102+
args.top_k,
103+
):
104+
print(f"{score:.4f} #{row_id}: {content}")
105+
106+
107+
if __name__ == "__main__":
108+
main()

0 commit comments

Comments
 (0)