{"record":{"id":"f0b7ec96c9b2d977","repo":"tursodatabase/turso","slug":"fts-index-not-used-for-case-details","errorCode":null,"errorMessage":"FTS index not used for {case:?}: {details:?}","messagePattern":"FTS index not used for (.+?): (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"perf/memory/src/fts.rs","lineNumber":505,"sourceCode":"            result.rows += 1;\n            result.id_sum += row.get::<i64>(0)?;\n        }\n        Ok(result)\n    }\n\n    async fn check_index(&self, case: QueryCase) -> Result<()> {\n        let mut rows = self\n            .conn\n            .query(&format!(\"EXPLAIN QUERY PLAN {}\", case.sql()), ())\n            .await?;\n        let mut indexed = false;\n        let mut details = Vec::new();\n        while let Some(row) = rows.next().await? {\n            let detail = row.get::<String>(3)?;\n            indexed |= detail == \"QUERY INDEX METHOD fts\";\n            details.push(detail);\n        }\n        ensure!(indexed, \"FTS index not used for {case:?}: {details:?}\");\n        Ok(())\n    }\n}\n\nimpl QueryCase {\n    pub fn sql(self) -> &'static str {\n        match self {\n            Self::Rare => \"SELECT id FROM docs WHERE fts_match(title, body, 'rare')\",\n            Self::Common => \"SELECT id FROM docs WHERE fts_match(title, body, 'common')\",\n            Self::And => \"SELECT id FROM docs WHERE fts_match(title, body, 'alpha AND beta')\",\n            Self::Or => \"SELECT id FROM docs WHERE fts_match(title, body, 'alpha OR beta')\",\n            Self::Phrase => \"SELECT id FROM docs WHERE fts_match(title, body, '\\\"common rare\\\"')\",\n            Self::Ranked => {\n                \"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\"\n            }\n        }\n    }\n}","sourceCodeStart":487,"sourceCodeEnd":523,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/perf/memory/src/fts.rs#L487-L523","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet db = turso::Builder::new_local(path)\n    .build()\n    .await?;\n// after\nlet db = turso::Builder::new_local(path)\n    .experimental_index_method(true)\n    .build()\n    .await?;","handlingStrategy":"try-catch","validationCode":"// verify the feature is enabled before building the fixture\nlet db = turso::Builder::new_local(path)\n    .experimental_index_method(true)\n    .build()\n    .await?;","typeGuard":"async fn fts_index_used(conn: &turso::Connection, sql: &str) -> anyhow::Result<bool> {\n    let mut rows = conn.query(&format!(\"EXPLAIN QUERY PLAN {sql}\"), ()).await?;\n    while let Some(row) = rows.next().await? {\n        if row.get::<String>(3)? == \"QUERY INDEX METHOD fts\" {\n            return Ok(true);\n        }\n    }\n    Ok(false)\n}","tryCatchPattern":"match fixture_or_session.check_index(case).await {\n    Ok(()) => {}\n    Err(e) if e.to_string().starts_with(\"FTS index not used\") => {\n        eprintln!(\"planner fell back to scan for {case:?}: enable experimental_index_method or update the plan-detail match\");\n        return Err(e);\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["fts","query-plan","benchmark","feature-flag"],"backgroundTag":"feature-not-enabled","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-09-13T18:13:59.796Z","contentChangedAt":"2026-09-13T18:13:59.796Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}