Skip to content

Commit 156644b

Browse files
committed
Add persistent tracking for semantic search performance and usage
1 parent 1fc41a7 commit 156644b

2 files changed

Lines changed: 92 additions & 60 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import uuid
2+
3+
from django.db import models
4+
from django.conf import settings
5+
6+
class SemanticSearchUsage(models.Model):
7+
"""
8+
Tracks performance metrics and usage data for embedding searches.
9+
"""
10+
guid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
11+
timestamp = models.DateTimeField(auto_now_add=True)
12+
query_text = models.TextField(blank=True, null=True, help_text="The search query text")
13+
document_name = models.TextField(blank=True, null=True, help_text="Document name filter if used")
14+
document_guid = models.UUIDField(blank=True, null=True, help_text="Document GUID filter if used")
15+
num_results_requested = models.IntegerField(default=10, help_text="Number of results requested")
16+
user = models.ForeignKey(
17+
settings.AUTH_USER_MODEL,
18+
on_delete=models.CASCADE,
19+
related_name='semantic_searches',
20+
null=True,
21+
blank=True,
22+
help_text="User who performed the search (null for unauthenticated users)"
23+
)
24+
encoding_time = models.FloatField(help_text="Time to encode query in seconds")
25+
db_query_time = models.FloatField(help_text="Time for database query in seconds")
26+
num_results_returned = models.IntegerField(help_text="Number of results returned")
27+
min_distance = models.FloatField(null=True, blank=True, help_text="Minimum L2 distance (null if no results)")
28+
max_distance = models.FloatField(null=True, blank=True, help_text="Maximum L2 distance (null if no results)")
29+
median_distance = models.FloatField(null=True, blank=True, help_text="Median L2 distance (null if no results)")
30+
31+
32+
class Meta:
33+
ordering = ['-timestamp']
34+
indexes = [
35+
models.Index(fields=['-timestamp']),
36+
models.Index(fields=['user', '-timestamp']),
37+
]
38+
39+
def __str__(self):
40+
total_time = self.encoding_time + self.db_query_time
41+
user_display = self.user.email if self.user else "Anonymous"
42+
return f"Search by {user_display} at {self.timestamp} ({total_time:.3f}s)"
Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import time
22
import logging
3+
from statistics import median
34

45
from pgvector.django import L2Distance
56

67
from .sentencetTransformer_model import TransformerModel
78
from ..models.model_embeddings import Embeddings
9+
from ..models.model_search_usage import SemanticSearchUsage
810

911
logger = logging.getLogger(__name__)
1012

1113
def get_closest_embeddings(
12-
user, message_data, document_name=None, guid=None, num_results=10, return_metrics=False
14+
user, message_data, document_name=None, guid=None, num_results=10
1315
):
1416
"""
1517
Find the closest embeddings to a given message for a specific user.
@@ -26,42 +28,27 @@ def get_closest_embeddings(
2628
Filter results to a specific document GUID (takes precedence over document_name)
2729
num_results : int, default 10
2830
Maximum number of results to return
29-
return_metrics : bool, default False
30-
If True, return a tuple of (results, metrics) instead of just results
3131
3232
Returns
3333
-------
34-
list[dict] or tuple[list[dict], dict]
35-
If return_metrics is False (default):
36-
List of dictionaries containing embedding results with keys:
37-
- name: document name
38-
- text: embedded text content
39-
- page_number: page number in source document
40-
- chunk_number: chunk number within the document
41-
- distance: L2 distance from query embedding
42-
- file_id: GUID of the source file
43-
44-
If return_metrics is True:
45-
Tuple of (results, metrics) where metrics is a dictionary containing:
46-
- encoding_time: Time to encode query (seconds)
47-
- db_query_time: Time for database query (seconds)
48-
- total_time: Total execution time (seconds)
49-
- num_results_returned: Number of results returned
50-
- min_distance: Minimum L2 distance
51-
- max_distance: Maximum L2 distance
52-
- avg_distance: Average L2 distance
34+
list[dict]
35+
List of dictionaries containing embedding results with keys:
36+
- name: document name
37+
- text: embedded text content
38+
- page_number: page number in source document
39+
- chunk_number: chunk number within the document
40+
- distance: L2 distance from query embedding
41+
- file_id: GUID of the source file
5342
"""
5443

55-
start_time = time.time()
56-
5744
encoding_start = time.time()
5845
transformerModel = TransformerModel.get_instance().model
5946
embedding_message = transformerModel.encode(message_data)
6047
encoding_time = time.time() - encoding_start
6148

6249
db_query_start = time.time()
6350

64-
# Start building the query based on the message's embedding
51+
# Django QuerySets are lazily evaluated
6552
closest_embeddings_query = (
6653
Embeddings.objects.filter(upload_file__uploaded_by=user)
6754
.annotate(
@@ -70,18 +57,18 @@ def get_closest_embeddings(
7057
.order_by("distance")
7158
)
7259

73-
# Filtering results to a document GUID takes precedence over filtering results to document name
60+
# Filtering to a document GUID takes precedence over a document name
7461
if guid:
7562
closest_embeddings_query = closest_embeddings_query.filter(
7663
upload_file__guid=guid
7764
)
7865
elif document_name:
7966
closest_embeddings_query = closest_embeddings_query.filter(name=document_name)
8067

81-
# Slice the results to limit to num_results
68+
# Slicing is equivalent to SQL's LIMIT clause
8269
closest_embeddings_query = closest_embeddings_query[:num_results]
8370

84-
# Format the results to be returned
71+
# Iterating evaluates the QuerySet and hits the database
8572
# TODO: Research improving the query evaluation performance
8673
results = [
8774
{
@@ -96,38 +83,41 @@ def get_closest_embeddings(
9683
]
9784

9885
db_query_time = time.time() - db_query_start
99-
total_time = time.time() - start_time
100-
101-
# Calculate distance/similarity statistics
102-
num_results_returned = len(results)
103-
104-
#TODO: Handle user having no uploaded docs or doc filtering returning no matches
105-
106-
distances = [r["distance"] for r in results]
107-
min_distance = min(distances)
108-
max_distance = max(distances)
109-
avg_distance = sum(distances) / num_results_returned
110-
111-
logger.info(
112-
f"Embedding search completed: "
113-
f"Encoding time: {encoding_time:.3f}s, "
114-
f"DB query time: {db_query_time:.3f}s, "
115-
f"Total time: {total_time:.3f}s, "
116-
f"Returned: {num_results_returned} results, "
117-
f"Distance range: [{min_distance:.3f}, {max_distance:.3f}], "
118-
f"Average distance: {avg_distance:.3f}"
119-
)
12086

121-
if return_metrics:
122-
metrics = {
123-
"encoding_time": encoding_time,
124-
"db_query_time": db_query_time,
125-
"total_time": total_time,
126-
"num_results_returned": num_results_returned,
127-
"min_distance": min_distance,
128-
"max_distance": max_distance,
129-
"avg_distance": avg_distance,
130-
}
131-
return results, metrics
87+
try:
88+
# Handle user having no uploaded docs or doc filtering returning no matches
89+
if results:
90+
distances = [r["distance"] for r in results]
91+
SemanticSearchUsage.objects.create(
92+
query_text=message_data,
93+
user=user if (user and user.is_authenticated) else None,
94+
document_guid=guid,
95+
document_name=document_name,
96+
num_results_requested=num_results,
97+
encoding_time=encoding_time,
98+
db_query_time=db_query_time,
99+
num_results_returned=len(results),
100+
max_distance=max(distances),
101+
median_distance=median(distances),
102+
min_distance=min(distances)
103+
)
104+
else:
105+
logger.warning("Semantic search returned no results")
106+
107+
SemanticSearchUsage.objects.create(
108+
query_text=message_data,
109+
user=user if (user and user.is_authenticated) else None,
110+
document_guid=guid,
111+
document_name=document_name,
112+
num_results_requested=num_results,
113+
encoding_time=encoding_time,
114+
db_query_time=db_query_time,
115+
num_results_returned=0,
116+
max_distance=None,
117+
median_distance=None,
118+
min_distance=None
119+
)
120+
except Exception as e:
121+
logger.error(f"Failed to create semantic search usage database record: {e}")
132122

133123
return results

0 commit comments

Comments
 (0)