|
2 | 2 | from sqlalchemy import select, delete |
3 | 3 | from typing import List, Optional, Dict, Any |
4 | 4 | from uuid import UUID |
| 5 | +import math |
| 6 | +import json |
5 | 7 |
|
6 | 8 | from backend.core.database import normalize_uuid, is_sqlite |
7 | 9 | from .models import KnowledgeBase, Document, Paragraph, Embedding |
@@ -204,11 +206,77 @@ async def search_embeddings_by_similarity( |
204 | 206 | db_kb_id = normalize_uuid(knowledge_base_id) if knowledge_base_id else None |
205 | 207 | db_app_id = normalize_uuid(application_id) if application_id else None |
206 | 208 |
|
207 | | - bind = db.get_bind() |
208 | | - dialect_name = getattr(bind.dialect, "name", None) if bind is not None else None |
209 | | - |
210 | | - if is_sqlite or dialect_name == "sqlite": |
211 | | - return [] |
| 209 | + if is_sqlite: |
| 210 | + # SQLite fallback: fetch candidate vectors and compute cosine in Python |
| 211 | + query = """ |
| 212 | + SELECT |
| 213 | + e.id as embedding_id, |
| 214 | + e.vector as embedding_vector, |
| 215 | + e.paragraph_id, |
| 216 | + p.content as paragraph_content, |
| 217 | + p.document_id, |
| 218 | + d.title as document_title, |
| 219 | + d.knowledge_base_id |
| 220 | + FROM embeddings e |
| 221 | + JOIN paragraphs p ON e.paragraph_id = p.id |
| 222 | + JOIN documents d ON p.document_id = d.id |
| 223 | + """ |
| 224 | + |
| 225 | + params = {} |
| 226 | + where_conditions = [] |
| 227 | + if db_kb_id: |
| 228 | + where_conditions.append("d.knowledge_base_id = :kb_id") |
| 229 | + params["kb_id"] = db_kb_id |
| 230 | + if db_app_id: |
| 231 | + where_conditions.append("d.application_id = :app_id") |
| 232 | + params["app_id"] = db_app_id |
| 233 | + |
| 234 | + if where_conditions: |
| 235 | + query += " WHERE " + " AND ".join(where_conditions) |
| 236 | + |
| 237 | + result = await db.execute(text(query), params) |
| 238 | + rows = result.fetchall() |
| 239 | + |
| 240 | + def _coerce_vector(raw_value): |
| 241 | + if raw_value is None: |
| 242 | + return [] |
| 243 | + if isinstance(raw_value, (bytes, bytearray, memoryview)): |
| 244 | + raw_value = raw_value.decode("utf-8") |
| 245 | + if isinstance(raw_value, str): |
| 246 | + try: |
| 247 | + raw_value = json.loads(raw_value) |
| 248 | + except json.JSONDecodeError: |
| 249 | + raw_value = [] |
| 250 | + return [float(x) for x in raw_value] |
| 251 | + |
| 252 | + def cosine_similarity(vec_a, vec_b): |
| 253 | + dot = sum(a * b for a, b in zip(vec_a, vec_b)) |
| 254 | + norm_a = math.sqrt(sum(a * a for a in vec_a)) |
| 255 | + norm_b = math.sqrt(sum(b * b for b in vec_b)) |
| 256 | + if not norm_a or not norm_b: |
| 257 | + return 0.0 |
| 258 | + return dot / (norm_a * norm_b) |
| 259 | + |
| 260 | + scored_rows = [] |
| 261 | + for row in rows: |
| 262 | + stored_vector = _coerce_vector(row.embedding_vector) |
| 263 | + score = cosine_similarity(stored_vector, query_vector) |
| 264 | + if threshold is None or score >= threshold: |
| 265 | + scored_rows.append( |
| 266 | + { |
| 267 | + "embedding_id": row.embedding_id, |
| 268 | + "paragraph_id": row.paragraph_id, |
| 269 | + "document_id": row.document_id, |
| 270 | + "knowledge_base_id": row.knowledge_base_id, |
| 271 | + "paragraph_content": row.paragraph_content, |
| 272 | + "document_title": row.document_title, |
| 273 | + "similarity_score": float(score), |
| 274 | + "embedding_vector": stored_vector, |
| 275 | + } |
| 276 | + ) |
| 277 | + |
| 278 | + scored_rows.sort(key=lambda r: r["similarity_score"], reverse=True) |
| 279 | + return scored_rows[:limit] |
212 | 280 |
|
213 | 281 | # PostgreSQL path with pgvector |
214 | 282 | query = """ |
@@ -289,9 +357,6 @@ async def search_paragraphs_by_text( |
289 | 357 | # Generate embedding for query text |
290 | 358 | query_vector = await encode_text(query_text) |
291 | 359 |
|
292 | | - if is_sqlite: |
293 | | - return [] |
294 | | - |
295 | 360 | # Search for similar embeddings |
296 | 361 | return await search_embeddings_by_similarity( |
297 | 362 | db, query_vector, limit, knowledge_base_id, application_id |
|
0 commit comments