원클릭으로
sqlite-search
Use when adding vector (sqlite-vec), full-text (FTS5), or hybrid RRF search to liteorm's SQLite backend.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when adding vector (sqlite-vec), full-text (FTS5), or hybrid RRF search to liteorm's SQLite backend.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when storing large or growing binary content (files, uploads, blobs) in SQLite as streamed io.ReaderAt/io.WriterAt instead of loading whole []byte — the orm.LOB field + liteorm.org/dialect/sqlite/lob.
Use when working with liteorm's declarative orm front-end — model structs and tags, AutoMigrate, the orm.Repo, associations (Load/Attach), hooks, or soft delete.
Use when liteorm code behaves unexpectedly, or proactively before writing it, to avoid the common gotchas around loading, soft delete, types, and dialect-gated operators.
Use when migrating a gorm codebase to liteorm — running models on gorm tags as-is, rewriting them to native orm tags, and adjusting for what differs.
Use when capturing, applying, inverting, or concatenating SQLite changesets (the SESSION extension) for audit logs, one-way replication, or undo — the liteorm.org/dialect/sqlite/changeset package. SQLite backend only.
Use when instrumenting liteorm with metrics, tracing, or audit around every executed statement — the Observer seam (liteorm.WithObserver + QueryEvent). Distinct from slog statement logging (the logging skill), which rides the same seam.
| name | sqlite-search |
| description | Use when adding vector (sqlite-vec), full-text (FTS5), or hybrid RRF search to liteorm's SQLite backend. |
Import liteorm.org/dialect/sqlite/search. These features are SQLite-only and capability-gated: the helpers take a session opened by liteorm.org/dialect/sqlite and return search.ErrUnsupportedBackend for any other dialect. Every index is a sidecar table keyed by the model's primary key.
Declare the indexes on the model; orm.AutoMigrate creates the sidecars and the triggers/hooks that keep them in sync, so plain Repo.Create/Update/Delete need no index calls. Search with the typed helpers, which return models in ranked order.
type Article struct {
ID int64
Title string
Body string
Embedding []float32 `orm:"-"` // sidecar-only (not a base column)
}
func (Article) SearchIndexes() []orm.SearchIndex {
return []orm.SearchIndex{
orm.FullText("Title", "Body"),
orm.Vector("Embedding", 384).WithMetric(orm.Cosine),
}
}
orm.AutoMigrate[Article](ctx, db) // creates articles + articles_fts + articles_vec (+sync)
repo := orm.NewRepo[Article](db)
repo.Create(ctx, &Article{Title: "…", Body: "…", Embedding: vec}) // sidecars sync automatically
s := search.For[Article](db) // typed searcher
near, _ := s.Vector(ctx, queryVec, 5) // vector
hits, _ := s.FullText(ctx, search.Term("rocket"), 5) // full-text
fused, _ := s.Hybrid(ctx, queryVec, search.Term("rocket"), 5) // hybrid (RRF)
for _, h := range near { use(h.Model, h.Score) }
h.Score: vector distance for .Vector (smaller nearer), BM25 rank for .FullText, RRF score for .Hybrid (larger better).search.For[Article](db).Field("FieldName").SearchIndexes() []orm.SearchIndex — full power: multi-column full-text, WithWeights, tokenizer/prefix/detail.vec:"dim=384;metric=cosine" on the embedding, fts:"tokenize=porter unicode61" on a text field (fts5: is an alias)..WithSync(...))query writes included.orm:"-" vector embedding — no column duplication): synced from the orm write path only; writes bypassing the orm are not indexed.Drive a sidecar by hand when there is no model, or you provision/backfill on your own schedule.
v, _ := search.NewVector(ctx, db, "doc_vecs", 5 /* dim */, search.Cosine) // or OpenVector to attach
_ = v.Add(ctx, id /* int64 */, emb /* []float32 */) // re-Add replaces
keys, _ := v.Search(ctx, queryEmb, 3) // []int64, nearest first
scored, _ := v.SearchScored(ctx, queryEmb, 3) // []Scored{Key, Score=distance}
f, _ := search.NewFullText(ctx, db, "doc_fts") // or OpenFullText(name, cols...)
_ = f.Add(ctx, id, title+" "+body)
keys, _ = f.Search(ctx, search.And(search.Term("software"), search.Term("flight")), 5)
fused, _ := search.Hybrid(ctx, v, f, queryEmb, search.Term("software"), 4) // []Scored{Key, Score=RRF}
docs, _ := search.Fetch[Doc](ctx, db, keys) // or FetchScored from []Scored; preserves order
Query builders (re-exported, no gosqlite import): Term, Phrase, Prefix, And, Or, Not, Near, Column, Raw. Metrics: orm.Cosine (the default via orm.Vector(...)), orm.L2, orm.L1, orm.Hamming. RRF tuning: search.WithK(60), search.WithWeights(wVec, wText).
*SQL + query.Raw[T]When your row shape diverges from the model — extra computed columns, a JOIN, a non-ranking filter, a Score/Distance projection — drop below search.For[T] and compose the SQL through gosqlite's typed fts.SearchSQL / vec.KNNSQL (with their WithSelect / WithJoin / WithFilter options), then execute it with query.Raw[T](ctx, sess, sql, args...). That is the canonical, intended executor for *SQL-returning APIs — the typed pipeline's final step, not a raw-SQL escape hatch. Reach for search.For[T] when you want model-shaped ranked results; drop one level down when the shape diverges.
sql, args, _ := vecTbl.KNNSQL(queryEmb, 10, vec.WithSelect("rowid, distance"), vec.WithFilter("source = ?", src))
hits, _ := query.Raw[searchHit](ctx, db, sql, args...)
query.Match("col", q) is the SQLite MATCH operator as a composable predicate — for filtering an FTS5 / spellfix1 / sqlite-vec virtual table inside an ordinary query.Select or orm.Repo chain, alongside OrderBy/Limit/other Filter predicates. It is feature-gated (rejected at build time off SQLite). Narrower than search.For[T] (which returns ranked models): use Match when you want the operator inside your own query, search.For[T] when you want ranking.
hit, _ := orm.NewRepo[Vocab](db).
Filter(query.Match("word", term), query.Col[int]("scope").Unvalidated().Le(2)).
OrderBy("distance ASC").
First(ctx)
scope is a vtab HIDDEN column (it constrains the search but isn't on the model), so the predicate column is marked .Unvalidated() — typed and quoted, but exempt from the model-schema validation that would otherwise reject it. The same applies to FTS5 / sqlite-vec hidden constraint columns.
A spellfix1 vocabulary is a global virtual table (not a per-model sidecar), so it isn't declared on a model — search.NewSpellfix gives a typed handle (create + populate + correct), no raw SQL:
sf, _ := search.NewSpellfix(ctx, db, "vocab") // create (idempotent)
_ = sf.Add(ctx, "kennedy", "jefferson", "lincoln") // one tx; dups ignored
hits, _ := sf.Correct(ctx, "kenedy", search.WithLimit(3)) // []Correction{Word,Distance,…}, nearest first
Also Size, Drop, OpenSpellfix, WithMaxDistance(n). Importing the search package registers the module. To fold a vocabulary match into a larger query, query.Match("word", term) works against the vtab directly (bind a model, OrderBy("distance ASC")).
gosqlite registers scalar functions globally, so they work through liteorm with no glue: blank-import gosqlite.org/ext/regexp/auto, then either write the operator inline — query.Select[T](db).Where("col REGEXP ?", pattern) — or use the sqlite.WhereRegex(column, pattern) helper, which returns the fragment and bind args and, when the pattern is left-anchored (^…), prepends a GLOB prefix so SQLite can range-scan an index on the column and run the RE2 match only on the survivors (an unanchored pattern falls back to a plain REGEXP scan):
frag, args := sqlite.WhereRegex("title", `^Intro to .* with Go$`)
rows, _ := query.Select[Doc](db).Where(frag, args...).All(ctx)
sqlite.RowidCol() is the implicit rowid as a typed query.Column[int64] (an .Unvalidated() column), and sqlite.Rowid() is the same as a projection query.Field — so a query can filter/order/pluck/project the row key without a raw query.Expr("rowid"):
rows, _ := query.Into[Item, reindexRow](ctx,
query.Select[Item](db).Order(query.Asc(sqlite.RowidCol())),
sqlite.Rowid(), query.Name("title")) // typed projection, no raw fragment
ids, _ := query.Pluck[Item, int64](ctx, query.Select[Item](db), sqlite.RowidCol())
On an INTEGER PRIMARY KEY table rowid is an alias of the PK column, so a Rowid projection reports the PK's name; it's most useful on string-keyed models where rowid is a distinct implicit column.
query insert won't be indexed.Score is larger-is-better for .Hybrid (RRF) but smaller-is-nearer for .Vector/SearchScored (distance). Don't compare across them.int64 primary key (FTS5 is keyed by the integer rowid; a string-PK model errors at migrate). Vector search supports both int64 and string PKs.dialect/sqlite session returns ErrUnsupportedBackend.ON CONFLICT. A vtab's xUpdate callback sees an INSERT but not the statement's ON CONFLICT … DO NOTHING clause (spellfix1, FTS5, sqlite-vec all ignore it), so an Upsert(..., OnConflict(...).DoNothing()) against a vtab won't dedup. For idempotent inserts use Create and swallow the duplicate — if err := repo.Create(ctx, &v); err != nil && !errors.Is(err, liteorm.ErrUniqueViolation) { return err } — or, for a spellfix1 vocabulary, search.NewSpellfix(...).Add(ctx, words...), whose vocabulary is a set — the spellfix1 virtual table drops duplicate words.