Skip to main content
Let’s get Infino running in a few minutes. We’ll build a small knowledge base (a handful of help-center notes) and search it four ways: by keyword, by meaning, by both at once (hybrid), and with SQL. Pick your language; each step builds on the one before it.
Prefer the terminal, or driving Infino from a coding agent? The CLI does all of this — create, ingest, search, and query — without writing code.
1

Install

pip install infino
# Or with uv (https://docs.astral.sh/uv/):
uv pip install infino
npm install @infino-ai/infino
cargo add infino
The Rust examples build Arrow data, and update / delete take DataFusion predicates (col, lit), so add these alongside infino, matching the Arrow and DataFusion versions Infino uses (Arrow 53 and DataFusion 53 for infino 0.1.x):
[dependencies]
infino = "0.1"
arrow-array = "53"
arrow-schema = "53"
datafusion-expr = "53"  # only needed for update / delete
Run cargo tree -p infino to check the versions if you are on a newer Infino.Infino also installs the mimalloc global allocator by default. If you embed it in a process that already sets a global allocator, turn it off: infino = { version = "0.1", default-features = false }.
2

Connect

First, open a connection. "memory://" keeps everything in memory for this walkthrough; point it at "./data" or an "s3://bucket/prefix" URI when you want your data to stick around.
import infino
import pyarrow as pa

db = infino.connect("memory://")
import { connect, IndexSpec } from "@infino-ai/infino";

const db = connect("memory://");
use std::sync::Arc;
use arrow_array::{FixedSizeListArray, Float32Array, LargeStringArray, RecordBatch};
use arrow_schema::{DataType, Field, Schema};
use infino::{connect, BoolMode, IndexSpec, Metric, VectorSearchOptions};

let db = connect("memory://")?;
3

Create a table

Now create a table. You give it a schema and say which columns to index: a full-text index on the text, and a vector index on the embedding.
schema = pa.schema([
    pa.field("source", pa.large_utf8(), nullable=False),
    pa.field("body", pa.large_utf8(), nullable=False),
    pa.field("embedding", pa.list_(pa.float32(), 16), nullable=False),
])
docs = db.create_table(
    "docs", schema,
    infino.IndexSpec().fts("body").vector("embedding", 16, 1, "cosine"),
)
const docs = db.createTable(
  "docs",
  { source: "large_utf8", body: "large_utf8", embedding: { vector: 16 } },
  new IndexSpec().fts("body").vector("embedding", 16, 1, "cosine"),
);
let item = Arc::new(Field::new("item", DataType::Float32, true));
let schema = Arc::new(Schema::new(vec![
    Field::new("source", DataType::LargeUtf8, false),
    Field::new("body", DataType::LargeUtf8, false),
    Field::new("embedding", DataType::FixedSizeList(item.clone(), 16), false),
]));
let docs = db.create_table(
    "docs",
    schema.clone(),
    IndexSpec::new().fts("body").vector("embedding", 16, 1, Metric::Cosine),
)?;
4

Add data

Let’s add a few notes. The embed helper stands in for a real embedding model so the example runs on its own. It’s a 16-dim one-hot by topic (0 = billing, 1 = appearance). Your own embeddings will be dense and higher-dimensional; see Embeddings.
def embed(topic):
    v = [0.0] * 16
    v[topic] = 1.0
    return v

docs.append([
    {"source": "help-center", "body": "To cancel a subscription, open Settings then Billing.", "embedding": embed(0)},
    {"source": "help-center", "body": "Refunds return to the original payment method.",         "embedding": embed(0)},
    {"source": "blog",        "body": "Enable dark mode under Settings then Appearance.",        "embedding": embed(1)},
])
const embed = (topic) => { const v = Array(16).fill(0.0); v[topic] = 1.0; return v; };

docs.append([
  { source: "help-center", body: "To cancel a subscription, open Settings then Billing.", embedding: embed(0) },
  { source: "help-center", body: "Refunds return to the original payment method.",         embedding: embed(0) },
  { source: "blog",        body: "Enable dark mode under Settings then Appearance.",        embedding: embed(1) },
]);
fn embed(topic: usize) -> Vec<f32> {
    let mut v = vec![0.0_f32; 16];
    v[topic] = 1.0;
    v
}

let flat: Vec<f32> = [0usize, 0, 1].iter().flat_map(|&t| embed(t)).collect();
docs.append(&RecordBatch::try_new(
    schema,
    vec![
        Arc::new(LargeStringArray::from(vec!["help-center", "help-center", "blog"])),
        Arc::new(LargeStringArray::from(vec![
            "To cancel a subscription, open Settings then Billing.",
            "Refunds return to the original payment method.",
            "Enable dark mode under Settings then Appearance.",
        ])),
        Arc::new(FixedSizeListArray::new(item, 16, Arc::new(Float32Array::from(flat)), None)),
    ],
)?)?;
5

Search it

Now the part you came for. The same table answers every kind of query: keyword, meaning, hybrid, and SQL.
keyword  = docs.bm25_search("body", "cancel subscription", 5)               # BM25
semantic = docs.vector_search("embedding", embed(0), 5)                     # vector kNN
# hybrid: BM25 + vector, fused with reciprocal-rank fusion:
hybrid   = docs.hybrid_search("body", "cancel subscription", "embedding", embed(0), 5)
billing  = db.query_sql("SELECT body FROM docs WHERE source = 'help-center'")  # SQL
const keyword  = docs.bm25Search("body", "cancel subscription", 5);            // BM25
const semantic = docs.vectorSearch("embedding", embed(0), 5);                  // vector kNN
// hybrid: BM25 + vector, fused with reciprocal-rank fusion:
const hybrid   = docs.hybridSearch("body", "cancel subscription", "embedding", embed(0), 5);
const billing  = db.querySql("SELECT body FROM docs WHERE source = 'help-center'");  // SQL
let keyword = docs.bm25_search("body", "cancel subscription", 5, BoolMode::Or, None)?; // BM25
let semantic =
    docs.vector_search("embedding", &embed(0), 5, VectorSearchOptions::new(), None, None)?; // vector kNN
// hybrid: BM25 + vector, fused with reciprocal-rank fusion:
let hybrid = docs.hybrid_search(
    "body", "cancel subscription", BoolMode::Or,
    "embedding", &embed(0), VectorSearchOptions::new(), 5, None,
)?;
let billing = db.query_sql("SELECT body FROM docs WHERE source = 'help-center'")?; // SQL
Each search returns Arrow rows. With this tiny corpus, the body of the rows you get back looks like this:
Expected output
keyword   "To cancel a subscription, open Settings then Billing."

semantic  "Refunds return to the original payment method."
          "To cancel a subscription, open Settings then Billing."
          "Enable dark mode under Settings then Appearance."

hybrid    "To cancel a subscription, open Settings then Billing."
          "Refunds return to the original payment method."
          "Enable dark mode under Settings then Appearance."

sql       "To cancel a subscription, open Settings then Billing."
          "Refunds return to the original payment method."
keyword matched the BM25 terms; semantic ranks the billing notes first; hybrid fuses the two rankings, so the exact keyword match leads while the semantically close note follows; sql returns the two help-center rows. From here, feed the retrieved passages to your model as grounding context.

Next steps

Core concepts

The mental model: one Parquet copy, indexed and queried four ways.

Guides

Tables, embeddings, indexing, search, and storage.

Integrations

LangChain, Vercel AI SDK, CrewAI, and MCP.

SQL Reference

Query and compose search with SQL.
Last modified on July 6, 2026