Build transactional source updates and deletions, then query the actual SQLite text index to check that retired chunks and deleted sources no longer appear.
When you need this. Use it when edited or deleted documents must stop returning stale hits from the text index.
Prerequisites. Complete the preceding ingestion module and use the supplied local files. These programs use local files and the Python standard library.
Keep the base rows and text index together
An indexed source has a source-revision row, chunk rows and corresponding FTS5 rows. FTS5 is SQLite’s full-text search extension. Here it supplies a real lexical index that makes stale search results observable. This lesson teaches index maintenance, not ranking or vector search.
The supplied snapshots come from the versioning lab. They contain trusted local Python API summaries and one explicitly labelled local teaching revision. The added paragraph contains saving, which is absent from the original snapshot. The driver uses that term to expose the update and deletion in actual MATCH query results.
The API accepts trusted version-builder records. It does not recompute their identities or authenticate uploaded snapshots. Use the preceding versioning contract before calling it. The separate vector retrieval course covers vector-store behavior; this text index does not replace that implementation.
Replace a source in one transaction
The replacement operation deletes the source’s prior FTS5 rows and chunk rows, updates its source revision, then inserts its new chunks into both the base table and the index. One transaction encloses all those changes. The repeated replacement produces the same stored content.
Interrupted replacement. An injected exception before commit rolls back the source, chunk and index changes. The old committed rows and query results remain. A successful return matters only after the transaction commits.
Check more than the source row. The tests assert that retired chunk IDs leave the stored index and that an old-only search term no longer matches after replacement. A clean-looking metadata record can otherwise hide stale search hits.
Delete the index entries as well as the source
Deletion removes FTS5 rows, chunk rows and the source row in one transaction. The driver’s next saving query no longer returns the deleted source. This is a query-visible removal check, not a generated-answer evaluation.
Interrupted deletion. An injected exception rolls back the deletion, preserving the previous source and index rows. Retrying after a successful deletion returns missing-no-op. The caller can distinguish the original deletion from an idempotent retry without inventing a new source.
Removing rows is not secure erasure. Old database pages, backups, caches and independent copies are outside this contract. The lab also supplies no multiwriter coordination, authorization layer or concurrent search-serving protocol.
Check the installed SQLite capability
The SQLite library linked to Python must include FTS5. If it does not, creating the virtual table raises sqlite3.OperationalError during setup. Use a prepared runtime with that capability; this lab does not download or compile an extension. Exact SQLite runtime versions stay in the raw execution record.
Execute the local corpus operation
Download the complete lab. The implementation, fixtures, tests, README and raw execution record travel together.
"""Transactional local source/chunk maintenance with a real SQLite FTS5 index."""
import sqlite3
def connect(path):
db = sqlite3.connect(path)
db.execute("PRAGMA foreign_keys=ON")
db.executescript("""
CREATE TABLE IF NOT EXISTS sources(source_id TEXT PRIMARY KEY, document_revision TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS chunks(chunk_id TEXT PRIMARY KEY, source_id TEXT NOT NULL REFERENCES sources(source_id), text TEXT NOT NULL);
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(chunk_id UNINDEXED, source_id UNINDEXED, text);
""")
return db
def replace_source(db, source, interrupt=False):
sid = source["source_id"]
with db:
db.execute("DELETE FROM search_index WHERE source_id=?", (sid,))
db.execute("DELETE FROM chunks WHERE source_id=?", (sid,))
db.execute("INSERT INTO sources VALUES (?,?) ON CONFLICT(source_id) DO UPDATE SET document_revision=excluded.document_revision", (sid, source["document_revision"]))
for chunk in source["chunks"]:
row = (chunk["chunk_id"], sid, chunk["text"])
db.execute("INSERT INTO chunks VALUES (?,?,?)", row)
db.execute("INSERT INTO search_index VALUES (?,?,?)", row)
if interrupt:
raise InterruptedError("injected interruption before index commit")
def delete_source(db, source_id, interrupt=False):
"""Deleting a missing source is an explicit idempotent no-op."""
with db:
present = db.execute("SELECT 1 FROM sources WHERE source_id=?", (source_id,)).fetchone() is not None
db.execute("DELETE FROM search_index WHERE source_id=?", (source_id,))
db.execute("DELETE FROM chunks WHERE source_id=?", (source_id,))
db.execute("DELETE FROM sources WHERE source_id=?", (source_id,))
if interrupt:
raise InterruptedError("injected interruption before deletion commit")
return "deleted" if present else "missing-no-op"
def search(db, query):
"""Return lexical matches; no ranking or dense-vector claim."""
return [dict(zip(("chunk_id", "source_id", "text"), row)) for row in db.execute("SELECT chunk_id,source_id,text FROM search_index WHERE search_index MATCH ? ORDER BY source_id,chunk_id", (query,))]
def state(db):
return {table: [list(row) for row in db.execute(f"SELECT * FROM {table} ORDER BY 1,2")] for table in ("sources", "chunks", "search_index")}python3 run.py --database /tmp/index-maintenance-demo.sqlite
python3 -m unittest -v test_index.py[
{
"operation": "load-original-snapshot",
"query": "saving",
"matches": []
},
{
"operation": "interrupt-replacement-before-commit",
"status": "rolled-back",
"query": "saving",
"matches": []
},
{
"operation": "replace-changed-source",
"query": "saving",
"matches": [
"path-write"
]
},
{
"operation": "interrupt-deletion-before-commit",
"status": "rolled-back",
"query": "saving",
"matches": [
"path-write"
]
},
{
"operation": "delete-source",
"outcome": "deleted",
"query": "saving",
"matches": []
},
{
"operation": "repeat-delete",
"outcome": "missing-no-op",
"query": "saving",
"matches": []
}
]Inspect the committed rows and operation outcomes in the complete saved result.
Inspect the complete saved result
{
"execution_mode": "offline-sqlite-source-maintenance",
"model_called": false,
"sqlite_version": "3.53.4",
"initial_snapshot_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"updated_snapshot_id": "d1ee6e652c1c6d08e6e4e10314f53153d6e0b5e5850739fd135f455196d4ec0d",
"steps": [
{
"operation": "load-original-snapshot",
"state": {
"sources": [
[
"json-text",
"0c4b565b2b1900787d6da9f8539c471f8cb2245c00abf887a57d460364abc57a"
],
[
"path-read",
"b58385b9bf597a951c16c035653d639929924e53eae3e5ada255afd6ee1592f2"
],
[
"path-read-3-13",
"d68df3e7ea1e24af3f40366a61402fc80558fefe2ef5f3eda928de0e01820db9"
],
[
"path-write",
"28c6349eaf80b629322edb310bb871dc45a04af9fad3b752bc75d12f0d7314e7"
]
],
"chunks": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
],
"search_index": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
]
},
"query": "saving",
"matches": []
},
{
"operation": "interrupt-replacement-before-commit",
"status": "rolled-back",
"error": "injected interruption before index commit",
"state": {
"sources": [
[
"json-text",
"0c4b565b2b1900787d6da9f8539c471f8cb2245c00abf887a57d460364abc57a"
],
[
"path-read",
"b58385b9bf597a951c16c035653d639929924e53eae3e5ada255afd6ee1592f2"
],
[
"path-read-3-13",
"d68df3e7ea1e24af3f40366a61402fc80558fefe2ef5f3eda928de0e01820db9"
],
[
"path-write",
"28c6349eaf80b629322edb310bb871dc45a04af9fad3b752bc75d12f0d7314e7"
]
],
"chunks": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
],
"search_index": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
]
},
"query": "saving",
"matches": []
},
{
"operation": "replace-changed-source",
"source_id": "path-write",
"retired_chunk_ids": [
"c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7"
],
"state": {
"sources": [
[
"json-text",
"0c4b565b2b1900787d6da9f8539c471f8cb2245c00abf887a57d460364abc57a"
],
[
"path-read",
"b58385b9bf597a951c16c035653d639929924e53eae3e5ada255afd6ee1592f2"
],
[
"path-read-3-13",
"d68df3e7ea1e24af3f40366a61402fc80558fefe2ef5f3eda928de0e01820db9"
],
[
"path-write",
"a036f69e0484af8c5ddd938bb292a0cb63ec2f661379d871d3901a03dc987dd2"
]
],
"chunks": [
[
"99e7deb4a60f1d2a3e3e8f33f3283b654e2ea80568aa6e2aa819d4666714c42f",
"path-write",
"Local teaching revision: callers must review overwrite behavior before saving.\n"
],
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"d27f0074d2d1a0c980e8bf541c26a83cf970e2f71e2c7f9a7d3da5442214b66b",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.\n"
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
],
"search_index": [
[
"99e7deb4a60f1d2a3e3e8f33f3283b654e2ea80568aa6e2aa819d4666714c42f",
"path-write",
"Local teaching revision: callers must review overwrite behavior before saving.\n"
],
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"d27f0074d2d1a0c980e8bf541c26a83cf970e2f71e2c7f9a7d3da5442214b66b",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.\n"
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
]
},
"query": "saving",
"matches": [
{
"chunk_id": "99e7deb4a60f1d2a3e3e8f33f3283b654e2ea80568aa6e2aa819d4666714c42f",
"source_id": "path-write",
"text": "Local teaching revision: callers must review overwrite behavior before saving.\n"
}
]
},
{
"operation": "interrupt-deletion-before-commit",
"status": "rolled-back",
"error": "injected interruption before deletion commit",
"state": {
"sources": [
[
"json-text",
"0c4b565b2b1900787d6da9f8539c471f8cb2245c00abf887a57d460364abc57a"
],
[
"path-read",
"b58385b9bf597a951c16c035653d639929924e53eae3e5ada255afd6ee1592f2"
],
[
"path-read-3-13",
"d68df3e7ea1e24af3f40366a61402fc80558fefe2ef5f3eda928de0e01820db9"
],
[
"path-write",
"a036f69e0484af8c5ddd938bb292a0cb63ec2f661379d871d3901a03dc987dd2"
]
],
"chunks": [
[
"99e7deb4a60f1d2a3e3e8f33f3283b654e2ea80568aa6e2aa819d4666714c42f",
"path-write",
"Local teaching revision: callers must review overwrite behavior before saving.\n"
],
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"d27f0074d2d1a0c980e8bf541c26a83cf970e2f71e2c7f9a7d3da5442214b66b",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.\n"
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
],
"search_index": [
[
"99e7deb4a60f1d2a3e3e8f33f3283b654e2ea80568aa6e2aa819d4666714c42f",
"path-write",
"Local teaching revision: callers must review overwrite behavior before saving.\n"
],
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"d27f0074d2d1a0c980e8bf541c26a83cf970e2f71e2c7f9a7d3da5442214b66b",
"path-write",
"Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.\n"
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
]
},
"query": "saving",
"matches": [
{
"chunk_id": "99e7deb4a60f1d2a3e3e8f33f3283b654e2ea80568aa6e2aa819d4666714c42f",
"source_id": "path-write",
"text": "Local teaching revision: callers must review overwrite behavior before saving.\n"
}
]
},
{
"operation": "delete-source",
"outcome": "deleted",
"state": {
"sources": [
[
"json-text",
"0c4b565b2b1900787d6da9f8539c471f8cb2245c00abf887a57d460364abc57a"
],
[
"path-read",
"b58385b9bf597a951c16c035653d639929924e53eae3e5ada255afd6ee1592f2"
],
[
"path-read-3-13",
"d68df3e7ea1e24af3f40366a61402fc80558fefe2ef5f3eda928de0e01820db9"
]
],
"chunks": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
],
"search_index": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
]
},
"query": "saving",
"matches": []
},
{
"operation": "repeat-delete",
"outcome": "missing-no-op",
"state": {
"sources": [
[
"json-text",
"0c4b565b2b1900787d6da9f8539c471f8cb2245c00abf887a57d460364abc57a"
],
[
"path-read",
"b58385b9bf597a951c16c035653d639929924e53eae3e5ada255afd6ee1592f2"
],
[
"path-read-3-13",
"d68df3e7ea1e24af3f40366a61402fc80558fefe2ef5f3eda928de0e01820db9"
]
],
"chunks": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
],
"search_index": [
[
"bdaeff5ae3ab88b70b12b9b6d4a0a46de45ea14520b971a7756cb03bbd384f35",
"path-read-3-13",
"In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
],
[
"c737db948e8111ae00e3442d8a22c80c876e43446dc1ee176ffe8ce142a41900",
"path-read",
"Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
],
[
"e5455737b11664449f653280d89dfc2faaa9a14de16e9c3eb24296ca2ceea51c",
"json-text",
"json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
]
]
},
"query": "saving",
"matches": []
}
]
}Compare the state before and after the operation
Run the demonstration with a fresh database path. Compare the actual saving matches before the replacement, after the replacement and after deletion. Inspect the retired chunk IDs as well as the text. Then compare the rollback tests: an interrupted replacement or deletion must leave every base and FTS5 row unchanged. Throughput, index size and concurrent readers remain unmeasured.
Read the explanation
Check the specific stored state and input contract; a completed command alone does not prove corpus consistency.
What’s next
Continue with How BM25 Ranks Documents.
References
APIs were checked on 23 October 2026 against official Python hashlib, Python sqlite3, SQLite transactions and SQLite FTS5 documentation as applicable. The source summaries retain their version-specific Python documentation URLs. The changed fixture is a labelled local teaching revision, not an upstream documentation update.