mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-28 06:39:01 +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
|
||||
|
||||
SQLITE_VEC_VERSION="0.1.3"
|
||||
SQLITE_VEC_VERSION="0.1.9"
|
||||
|
||||
source /etc/os-release
|
||||
|
||||
|
||||
+15
-10
@@ -1538,15 +1538,18 @@ async def set_description(
|
||||
event.data["description"] = new_description
|
||||
event.save()
|
||||
|
||||
# If semantic search is enabled, update the index
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
context: EmbeddingsContext | None = request.app.embeddings
|
||||
|
||||
if context is not None:
|
||||
if len(new_description) > 0:
|
||||
context.update_description(
|
||||
event_id,
|
||||
new_description,
|
||||
)
|
||||
# If semantic search is enabled, update the index
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context.update_description(
|
||||
event_id,
|
||||
new_description,
|
||||
)
|
||||
else:
|
||||
# embeddings are always cleaned up so they don't outlive their description
|
||||
context.db.delete_embeddings_description(event_ids=[event_id])
|
||||
|
||||
response_message = (
|
||||
@@ -1675,9 +1678,11 @@ async def delete_single_event(event_id: str, request: Request) -> dict:
|
||||
event.delete_instance()
|
||||
Timeline.delete().where(Timeline.source_id == event_id).execute()
|
||||
|
||||
# If semantic search is enabled, update the index
|
||||
if request.app.frigate_config.semantic_search.enabled:
|
||||
context: EmbeddingsContext = request.app.embeddings
|
||||
# embeddings are always cleaned up, even when semantic search is disabled,
|
||||
# so that they don't outlive their events
|
||||
context: EmbeddingsContext | None = request.app.embeddings
|
||||
|
||||
if context is not None:
|
||||
context.db.delete_embeddings_thumbnail(event_ids=[event_id])
|
||||
context.db.delete_embeddings_description(event_ids=[event_id])
|
||||
|
||||
|
||||
+1
-1
@@ -270,7 +270,7 @@ class FrigateApp:
|
||||
10
|
||||
* 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 = [
|
||||
Event,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Any
|
||||
|
||||
import regex
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REGEXP_TIMEOUT_SECONDS = 1.0
|
||||
|
||||
|
||||
@@ -28,8 +31,14 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
||||
|
||||
def _load_vec_extension(self, conn: sqlite3.Connection) -> None:
|
||||
conn.enable_load_extension(True)
|
||||
conn.load_extension(self.sqlite_vec_path)
|
||||
conn.enable_load_extension(False)
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def _register_regexp(self, conn: sqlite3.Connection) -> None:
|
||||
def regexp(expr: str, item: str | None) -> bool:
|
||||
@@ -44,13 +53,33 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
||||
|
||||
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])
|
||||
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:
|
||||
ids = ",".join(["?" for _ in event_ids])
|
||||
self.execute_sql(f"DELETE FROM vec_descriptions WHERE id IN ({ids})", event_ids)
|
||||
self._delete_embeddings("vec_descriptions", event_ids)
|
||||
|
||||
def drop_embeddings_tables(self) -> None:
|
||||
self.execute_sql("""
|
||||
|
||||
@@ -366,9 +366,10 @@ class EventCleanup(threading.Thread):
|
||||
logger.debug(f"Deleting {len(chunk)} events from the database")
|
||||
Event.delete().where(Event.id << chunk).execute()
|
||||
|
||||
if self.config.semantic_search.enabled:
|
||||
self.db.delete_embeddings_description(event_ids=chunk)
|
||||
self.db.delete_embeddings_thumbnail(event_ids=chunk)
|
||||
logger.debug(f"Deleted {len(ids_to_delete)} embeddings")
|
||||
# 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_thumbnail(event_ids=chunk)
|
||||
logger.debug(f"Deleted {len(chunk)} embeddings")
|
||||
|
||||
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