mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-04 09:58:51 +03:00
bump sqlite-vec to 0.1.9 and always clean up embeddings when events are deleted
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
SQLITE_VEC_VERSION="0.1.3"
|
SQLITE_VEC_VERSION="0.1.9"
|
||||||
|
|
||||||
source /etc/os-release
|
source /etc/os-release
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -1538,15 +1538,18 @@ async def set_description(
|
|||||||
event.data["description"] = new_description
|
event.data["description"] = new_description
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
|
context: EmbeddingsContext | None = request.app.embeddings
|
||||||
|
|
||||||
|
if context is not None:
|
||||||
|
if len(new_description) > 0:
|
||||||
# If semantic search is enabled, update the index
|
# If semantic search is enabled, update the index
|
||||||
if request.app.frigate_config.semantic_search.enabled:
|
if request.app.frigate_config.semantic_search.enabled:
|
||||||
context: EmbeddingsContext = request.app.embeddings
|
|
||||||
if len(new_description) > 0:
|
|
||||||
context.update_description(
|
context.update_description(
|
||||||
event_id,
|
event_id,
|
||||||
new_description,
|
new_description,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# embeddings are always cleaned up so they don't outlive their description
|
||||||
context.db.delete_embeddings_description(event_ids=[event_id])
|
context.db.delete_embeddings_description(event_ids=[event_id])
|
||||||
|
|
||||||
response_message = (
|
response_message = (
|
||||||
@@ -1675,9 +1678,11 @@ async def delete_single_event(event_id: str, request: Request) -> dict:
|
|||||||
event.delete_instance()
|
event.delete_instance()
|
||||||
Timeline.delete().where(Timeline.source_id == event_id).execute()
|
Timeline.delete().where(Timeline.source_id == event_id).execute()
|
||||||
|
|
||||||
# If semantic search is enabled, update the index
|
# embeddings are always cleaned up, even when semantic search is disabled,
|
||||||
if request.app.frigate_config.semantic_search.enabled:
|
# so that they don't outlive their events
|
||||||
context: EmbeddingsContext = request.app.embeddings
|
context: EmbeddingsContext | None = request.app.embeddings
|
||||||
|
|
||||||
|
if context is not None:
|
||||||
context.db.delete_embeddings_thumbnail(event_ids=[event_id])
|
context.db.delete_embeddings_thumbnail(event_ids=[event_id])
|
||||||
context.db.delete_embeddings_description(event_ids=[event_id])
|
context.db.delete_embeddings_description(event_ids=[event_id])
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -270,7 +270,7 @@ class FrigateApp:
|
|||||||
10
|
10
|
||||||
* len([c for c in self.config.cameras.values() if c.enabled_in_config]),
|
* len([c for c in self.config.cameras.values() if c.enabled_in_config]),
|
||||||
),
|
),
|
||||||
load_vec_extension=self.config.semantic_search.enabled,
|
load_vec_extension=True,
|
||||||
)
|
)
|
||||||
models = [
|
models = [
|
||||||
Event,
|
Event,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import regex
|
import regex
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
REGEXP_TIMEOUT_SECONDS = 1.0
|
REGEXP_TIMEOUT_SECONDS = 1.0
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +31,13 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
|||||||
|
|
||||||
def _load_vec_extension(self, conn: sqlite3.Connection) -> None:
|
def _load_vec_extension(self, conn: sqlite3.Connection) -> None:
|
||||||
conn.enable_load_extension(True)
|
conn.enable_load_extension(True)
|
||||||
|
|
||||||
|
try:
|
||||||
conn.load_extension(self.sqlite_vec_path)
|
conn.load_extension(self.sqlite_vec_path)
|
||||||
|
except conn.OperationalError:
|
||||||
|
logger.error("Unable to load the sqlite-vec extension")
|
||||||
|
self.load_vec_extension = False
|
||||||
|
finally:
|
||||||
conn.enable_load_extension(False)
|
conn.enable_load_extension(False)
|
||||||
|
|
||||||
def _register_regexp(self, conn: sqlite3.Connection) -> None:
|
def _register_regexp(self, conn: sqlite3.Connection) -> None:
|
||||||
@@ -44,13 +53,33 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
|||||||
|
|
||||||
conn.create_function("REGEXP", 2, regexp)
|
conn.create_function("REGEXP", 2, regexp)
|
||||||
|
|
||||||
def delete_embeddings_thumbnail(self, event_ids: list[str]) -> None:
|
def _delete_embeddings(self, table: str, event_ids: list[str]) -> None:
|
||||||
|
"""Delete embeddings for the given events, if the table exists.
|
||||||
|
|
||||||
|
Embeddings outlive the events they belong to when semantic search is
|
||||||
|
disabled, so deletes are attempted regardless of the current config.
|
||||||
|
"""
|
||||||
|
if not event_ids or not self.load_vec_extension:
|
||||||
|
return
|
||||||
|
|
||||||
|
# the embeddings tables are only created once semantic search has run
|
||||||
|
cursor = self.execute_sql(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
|
||||||
|
(table,),
|
||||||
|
)
|
||||||
|
|
||||||
|
if cursor.fetchone() is None:
|
||||||
|
logger.debug("Skipping %s cleanup, table does not exist", table)
|
||||||
|
return
|
||||||
|
|
||||||
ids = ",".join(["?" for _ in event_ids])
|
ids = ",".join(["?" for _ in event_ids])
|
||||||
self.execute_sql(f"DELETE FROM vec_thumbnails WHERE id IN ({ids})", event_ids)
|
self.execute_sql(f"DELETE FROM {table} WHERE id IN ({ids})", event_ids)
|
||||||
|
|
||||||
|
def delete_embeddings_thumbnail(self, event_ids: list[str]) -> None:
|
||||||
|
self._delete_embeddings("vec_thumbnails", event_ids)
|
||||||
|
|
||||||
def delete_embeddings_description(self, event_ids: list[str]) -> None:
|
def delete_embeddings_description(self, event_ids: list[str]) -> None:
|
||||||
ids = ",".join(["?" for _ in event_ids])
|
self._delete_embeddings("vec_descriptions", event_ids)
|
||||||
self.execute_sql(f"DELETE FROM vec_descriptions WHERE id IN ({ids})", event_ids)
|
|
||||||
|
|
||||||
def drop_embeddings_tables(self) -> None:
|
def drop_embeddings_tables(self) -> None:
|
||||||
self.execute_sql("""
|
self.execute_sql("""
|
||||||
|
|||||||
@@ -366,9 +366,10 @@ class EventCleanup(threading.Thread):
|
|||||||
logger.debug(f"Deleting {len(chunk)} events from the database")
|
logger.debug(f"Deleting {len(chunk)} events from the database")
|
||||||
Event.delete().where(Event.id << chunk).execute()
|
Event.delete().where(Event.id << chunk).execute()
|
||||||
|
|
||||||
if self.config.semantic_search.enabled:
|
# embeddings are always cleaned up, even when semantic search
|
||||||
|
# is disabled, so that they don't outlive their events
|
||||||
self.db.delete_embeddings_description(event_ids=chunk)
|
self.db.delete_embeddings_description(event_ids=chunk)
|
||||||
self.db.delete_embeddings_thumbnail(event_ids=chunk)
|
self.db.delete_embeddings_thumbnail(event_ids=chunk)
|
||||||
logger.debug(f"Deleted {len(ids_to_delete)} embeddings")
|
logger.debug(f"Deleted {len(chunk)} embeddings")
|
||||||
|
|
||||||
logger.info("Exiting event cleanup...")
|
logger.info("Exiting event cleanup...")
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Tests for embedding cleanup on the main Frigate database.
|
||||||
|
|
||||||
|
Embeddings are deleted whether or not semantic search is currently enabled, so
|
||||||
|
the delete path has to tolerate databases where the vec0 tables were never
|
||||||
|
created and installs where the sqlite-vec extension is unavailable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeleteEmbeddings(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.tmp_dir = tempfile.TemporaryDirectory()
|
||||||
|
self.db = SqliteVecQueueDatabase(os.path.join(self.tmp_dir.name, "test.db"))
|
||||||
|
self.db.start()
|
||||||
|
# the extension is not available to tests, so stand in for a database
|
||||||
|
# that has it loaded and use a plain table for the deletes
|
||||||
|
self.db.load_vec_extension = True
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.db.stop()
|
||||||
|
self.db.close()
|
||||||
|
self.tmp_dir.cleanup()
|
||||||
|
|
||||||
|
def _flush_writes(self) -> None:
|
||||||
|
# writes are queued and applied by a worker thread, and the queue is
|
||||||
|
# FIFO, so awaiting a later write means the earlier ones are done
|
||||||
|
self.db.execute_sql("PRAGMA user_version = 0").fetchall()
|
||||||
|
|
||||||
|
def _create_thumbnails_table(self) -> None:
|
||||||
|
self.db.execute_sql("CREATE TABLE vec_thumbnails (id TEXT PRIMARY KEY)")
|
||||||
|
self.db.execute_sql("INSERT INTO vec_thumbnails (id) VALUES ('a'), ('b')")
|
||||||
|
self._flush_writes()
|
||||||
|
|
||||||
|
def _thumbnail_ids(self) -> list[str]:
|
||||||
|
return [row[0] for row in self.db.execute_sql("SELECT id FROM vec_thumbnails")]
|
||||||
|
|
||||||
|
def test_delete_without_tables_does_not_raise(self) -> None:
|
||||||
|
# semantic search was never enabled, so event cleanup has nothing to do
|
||||||
|
self.db.delete_embeddings_thumbnail(event_ids=["1700000000.0-abc"])
|
||||||
|
self.db.delete_embeddings_description(event_ids=["1700000000.0-abc"])
|
||||||
|
|
||||||
|
def test_delete_removes_embeddings(self) -> None:
|
||||||
|
self._create_thumbnails_table()
|
||||||
|
|
||||||
|
self.db.delete_embeddings_thumbnail(event_ids=["a"])
|
||||||
|
self._flush_writes()
|
||||||
|
|
||||||
|
self.assertEqual(self._thumbnail_ids(), ["b"])
|
||||||
|
|
||||||
|
def test_delete_skipped_without_extension(self) -> None:
|
||||||
|
self._create_thumbnails_table()
|
||||||
|
self.db.load_vec_extension = False
|
||||||
|
|
||||||
|
self.db.delete_embeddings_thumbnail(event_ids=["a"])
|
||||||
|
self._flush_writes()
|
||||||
|
|
||||||
|
# the vec0 tables cannot be written without the extension
|
||||||
|
self.assertEqual(self._thumbnail_ids(), ["a", "b"])
|
||||||
Reference in New Issue
Block a user