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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-09-13). Data as JSON: /api/errors/f0b7ec96c9b2d977. Report an issue: GitHub.