tursodatabase/turso · error
FTS index not used for {case:?}: {details:?}
Error message
FTS index not used for {case:?}: {details:?} What it means
check_index runs `EXPLAIN QUERY PLAN` for a benchmark QueryCase and requires that at least one plan row's detail equals "QUERY INDEX METHOD fts", proving the FTS index (not a table scan) would serve the query. If no plan row carries that marker, the benchmark would measure a full scan instead of FTS behavior, so it fails with the case name and all plan details. This is a self-check executed by FtsFixture::create for every QueryCase variant after building the index.
Source
Thrown at perf/memory/src/fts.rs:505
result.rows += 1;
result.id_sum += row.get::<i64>(0)?;
}
Ok(result)
}
async fn check_index(&self, case: QueryCase) -> Result<()> {
let mut rows = self
.conn
.query(&format!("EXPLAIN QUERY PLAN {}", case.sql()), ())
.await?;
let mut indexed = false;
let mut details = Vec::new();
while let Some(row) = rows.next().await? {
let detail = row.get::<String>(3)?;
indexed |= detail == "QUERY INDEX METHOD fts";
details.push(detail);
}
ensure!(indexed, "FTS index not used for {case:?}: {details:?}");
Ok(())
}
}
impl QueryCase {
pub fn sql(self) -> &'static str {
match self {
Self::Rare => "SELECT id FROM docs WHERE fts_match(title, body, 'rare')",
Self::Common => "SELECT id FROM docs WHERE fts_match(title, body, 'common')",
Self::And => "SELECT id FROM docs WHERE fts_match(title, body, 'alpha AND beta')",
Self::Or => "SELECT id FROM docs WHERE fts_match(title, body, 'alpha OR beta')",
Self::Phrase => "SELECT id FROM docs WHERE fts_match(title, body, '\"common rare\"')",
Self::Ranked => {
"SELECT id, fts_score(title, body, 'alpha OR beta') AS score FROM docs WHERE fts_match(title, body, 'alpha OR beta') ORDER BY score DESC LIMIT 10"
}
}
}
}View on GitHub (pinned to 492c4a71cd)
Solutions
- Ensure the connection is opened with .experimental_index_method(true) (see FtsFixture::open) and index_stats uses with_index_method(true); without the feature the planner cannot choose FTS.
- Read the details vector in the error message: it shows the actual query plan; if it shows a scan over docs, rebuild the FTS index (CREATE INDEX docs_fts ON docs USING fts (title, body)) and verify it succeeded.
- If the plan genuinely uses FTS but the string changed, update the exact match `detail == "QUERY INDEX METHOD fts"` in check_index to the new plan wording after an engine change.
- Confirm the query uses fts_match on the indexed columns (title, body) exactly as QueryCase::sql does; custom SQL variants won't be planned via FTS.
Example fix
// before
let db = turso::Builder::new_local(path)
.build()
.await?;
// after
let db = turso::Builder::new_local(path)
.experimental_index_method(true)
.build()
.await?; Defensive patterns
Strategy: try-catch
Validate before calling
// verify the feature is enabled before building the fixture
let db = turso::Builder::new_local(path)
.experimental_index_method(true)
.build()
.await?; Type guard
async fn fts_index_used(conn: &turso::Connection, sql: &str) -> anyhow::Result<bool> {
let mut rows = conn.query(&format!("EXPLAIN QUERY PLAN {sql}"), ()).await?;
while let Some(row) = rows.next().await? {
if row.get::<String>(3)? == "QUERY INDEX METHOD fts" {
return Ok(true);
}
}
Ok(false)
} Try / catch
match fixture_or_session.check_index(case).await {
Ok(()) => {}
Err(e) if e.to_string().starts_with("FTS index not used") => {
eprintln!("planner fell back to scan for {case:?}: enable experimental_index_method or update the plan-detail match");
return Err(e);
}
Err(e) => return Err(e),
} Prevention
- Always enable experimental_index_method(true) when opening benchmark connections; without it FTS planning is impossible.
- Keep QueryCase::sql using fts_match/fts_score on the indexed columns (title, body); arbitrary SQL won't be FTS-planned.
- When upgrading the engine, re-run check_index early and compare the reported plan details to the exact expected string "QUERY INDEX METHOD fts" and update it if the wording changed.
When it happens
Trigger: Creating the fixture when the experimental index-method feature is not enabled (Builder::new_local(...).experimental_index_method(true) missing or the engine flag turso_core::DatabaseOpts::with_index_method(true) absent), so the planner never picks the FTS index for fts_match/fts_score queries; the FTS index failed to build (CREATE INDEX docs_fts ... USING fts silently not usable); or a planner/optimizer change makes the query plan fall back to a scan so the EXPLAIN detail string no longer matches.
Common situations: Opening the benchmark DB without the index-method feature flag; running against an engine build where FTS index selection regressed; the EXPLAIN QUERY PLAN detail wording changed so the exact-string comparison `detail == "QUERY INDEX METHOD fts"` fails even though FTS is used; querying a table whose FTS index creation didn't take effect.
Related errors
- index contains {} segment bytes, below requested minimum {};
- transactions must overlap before queries start
- documents must be positive
- connections must be positive
- missing FTS segment data
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-09-13).
Data as JSON: /api/errors/f0b7ec96c9b2d977.
Report an issue: GitHub.