Skip to content

Commit 3db95c1

Browse files
author
Daniele Briggi
committed
chore: new settings and search output
1 parent f90af0d commit 3db95c1

File tree

4 files changed

+303
-72
lines changed

4 files changed

+303
-72
lines changed

src/sqlite_rag/cli.py

Lines changed: 26 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import shlex
44
import sys
5+
import time
56
from pathlib import Path
67
from typing import Optional
78

@@ -10,6 +11,7 @@
1011
from sqlite_rag.database import Database
1112
from sqlite_rag.settings import SettingsManager
1213

14+
from .formatters import get_formatter
1315
from .sqliterag import SQLiteRag
1416

1517

@@ -120,10 +122,10 @@ def add(
120122
recursive: bool = typer.Option(
121123
False, "-r", "--recursive", help="Recursively add all files in directories"
122124
),
123-
absolute_paths: bool = typer.Option(
124-
False,
125-
"--absolute-paths",
126-
help="Store absolute paths instead of relative paths",
125+
use_relative_paths: bool = typer.Option(
126+
True,
127+
"--relative-paths",
128+
help="Store relative paths instead of absolute paths",
127129
),
128130
metadata: Optional[str] = typer.Option(
129131
None,
@@ -133,14 +135,19 @@ def add(
133135
),
134136
):
135137
"""Add a file path to the database"""
138+
start_time = time.time()
139+
136140
rag = SQLiteRag.create()
137141
rag.add(
138142
path,
139143
recursive=recursive,
140-
use_absolute_paths=absolute_paths,
144+
use_relative_paths=use_relative_paths,
141145
metadata=json.loads(metadata or "{}"),
142146
)
143147

148+
elapsed_time = time.time() - start_time
149+
typer.echo(f"{elapsed_time:.2f} seconds")
150+
144151

145152
@app.command()
146153
def add_text(
@@ -281,68 +288,28 @@ def search(
281288
query: str,
282289
limit: int = typer.Option(10, help="Number of results to return"),
283290
debug: bool = typer.Option(
284-
False, "-d", "--debug", help="Print extra debug information"
291+
False,
292+
"-d",
293+
"--debug",
294+
help="Print extra debug information with modern formatting",
295+
),
296+
peek: bool = typer.Option(
297+
False, "--peek", help="Print debug information using compact table format"
285298
),
286299
):
287300
"""Search for documents using hybrid vector + full-text search"""
301+
start_time = time.time()
302+
288303
rag = SQLiteRag.create()
289304
results = rag.search(query, top_k=limit)
290305

291-
if not results:
292-
typer.echo("No documents found matching the query.")
293-
return
306+
search_time = time.time() - start_time
294307

295-
typer.echo(f"Found {len(results)} documents:")
308+
# Get the appropriate formatter and display results
309+
formatter = get_formatter(debug=debug, table_view=peek)
310+
formatter.format_results(results, query)
296311

297-
if debug:
298-
# Enhanced debug table with better formatting
299-
typer.echo(
300-
f"{'#':<3} {'Preview':<55} {'URI':<35} {'C.Rank':<33} {'V.Rank':<8} {'FTS.Rank':<9} {'V.Dist':<18} {'FTS.Score':<18}"
301-
)
302-
typer.echo("─" * 180)
303-
304-
for idx, doc in enumerate(results, 1):
305-
# Clean snippet display
306-
snippet = doc.snippet.replace("\n", " ").replace("\r", "")
307-
if len(snippet) > 52:
308-
snippet = snippet[:49] + "..."
309-
310-
# Clean URI display
311-
uri = doc.document.uri or "N/A"
312-
if len(uri) > 32:
313-
uri = "..." + uri[-29:]
314-
315-
# Format debug values with proper precision
316-
c_rank = (
317-
f"{doc.combined_rank:.17f}" if doc.combined_rank is not None else "N/A"
318-
)
319-
v_rank = str(doc.vec_rank) if doc.vec_rank is not None else "N/A"
320-
fts_rank = str(doc.fts_rank) if doc.fts_rank is not None else "N/A"
321-
v_dist = (
322-
f"{doc.vec_distance:.6f}" if doc.vec_distance is not None else "N/A"
323-
)
324-
fts_score = f"{doc.fts_score:.6f}" if doc.fts_score is not None else "N/A"
325-
326-
typer.echo(
327-
f"{idx:<3} {snippet:<55} {uri:<35} {c_rank:<33} {v_rank:<8} {fts_rank:<9} {v_dist:<18} {fts_score:<18}"
328-
)
329-
else:
330-
# Clean simple table for normal view
331-
typer.echo(f"{'#':<3} {'Preview':<60} {'URI':<40}")
332-
typer.echo("─" * 105)
333-
334-
for idx, doc in enumerate(results, 1):
335-
# Clean snippet display
336-
snippet = doc.snippet.replace("\n", " ").replace("\r", "")
337-
if len(snippet) > 57:
338-
snippet = snippet[:54] + "..."
339-
340-
# Clean URI display
341-
uri = doc.document.uri or "N/A"
342-
if len(uri) > 37:
343-
uri = "..." + uri[-34:]
344-
345-
typer.echo(f"{idx:<3} {snippet:<60} {uri:<40}")
312+
typer.echo(f"{search_time:.3f} seconds")
346313

347314

348315
@app.command()

src/sqlite_rag/formatters.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#!/usr/bin/env python3
2+
"""Output formatters for CLI search results."""
3+
4+
from abc import ABC, abstractmethod
5+
from typing import List
6+
7+
import typer
8+
9+
from .models.document_result import DocumentResult
10+
11+
12+
class SearchResultFormatter(ABC):
13+
"""Base class for search result formatters."""
14+
15+
@abstractmethod
16+
def format_results(self, results: List[DocumentResult], query: str) -> None:
17+
"""Format and display search results."""
18+
19+
20+
class ModernCompactFormatter(SearchResultFormatter):
21+
"""Modern compact formatter with better space utilization."""
22+
23+
def format_results(self, results: List[DocumentResult], query: str) -> None:
24+
if not results:
25+
typer.echo("No documents found matching the query.")
26+
return
27+
28+
typer.echo(f"━━━ Search Results ({len(results)} matches) ━━━")
29+
typer.echo()
30+
31+
for idx, doc in enumerate(results, 1):
32+
# Get file type icon
33+
icon = self._get_file_icon(doc.document.uri or "")
34+
35+
# Format title (use filename or "Text Content")
36+
self._get_document_title(doc.document.uri or "")
37+
38+
# Clean and format content snippet
39+
snippet = self._clean_snippet(doc.snippet, max_length=200)
40+
41+
# Display the result
42+
typer.echo(f"[{idx}]")
43+
if doc.document.uri:
44+
typer.echo(f" {icon} {doc.document.uri}")
45+
typer.echo(f" {snippet}")
46+
typer.echo(" " + "─" * 80)
47+
48+
def _get_file_icon(self, uri: str) -> str:
49+
"""Get appropriate icon for file type."""
50+
if not uri:
51+
return "📝"
52+
53+
uri_lower = uri.lower()
54+
if uri_lower.endswith((".py", ".pyx")):
55+
return "🐍"
56+
elif uri_lower.endswith((".js", ".ts", ".jsx", ".tsx")):
57+
return "⚡"
58+
elif uri_lower.endswith((".md", ".markdown")):
59+
return "📄"
60+
elif uri_lower.endswith((".json", ".yaml", ".yml")):
61+
return "📋"
62+
elif uri_lower.endswith((".html", ".htm")):
63+
return "🌐"
64+
elif uri_lower.endswith((".css", ".scss", ".sass")):
65+
return "🎨"
66+
elif uri_lower.endswith((".txt", ".log")):
67+
return "📃"
68+
elif uri_lower.endswith((".pdf",)):
69+
return "📕"
70+
elif uri_lower.endswith((".sql",)):
71+
return "🗃️"
72+
else:
73+
return "📄"
74+
75+
def _get_document_title(self, uri: str) -> str:
76+
"""Extract a readable title from the document URI."""
77+
if not uri:
78+
return "Text Content"
79+
80+
# Extract filename from path
81+
filename = uri.split("/")[-1] if "/" in uri else uri
82+
# Remove extension for cleaner display
83+
if "." in filename:
84+
return (
85+
filename.rsplit(".", 1)[0].replace("_", " ").replace("-", " ").title()
86+
)
87+
return filename.replace("_", " ").replace("-", " ").title()
88+
89+
def _clean_snippet(self, snippet: str, max_length: int = 200) -> str:
90+
"""Clean and truncate snippet for display."""
91+
# Replace newlines and multiple spaces
92+
clean = snippet.replace("\n", " ").replace("\r", "")
93+
# Collapse multiple spaces
94+
clean = " ".join(clean.split())
95+
96+
if len(clean) > max_length:
97+
clean = clean[: max_length - 3] + "..."
98+
99+
return clean
100+
101+
102+
class ModernDetailedFormatter(SearchResultFormatter):
103+
"""Modern detailed formatter with debug information in boxes."""
104+
105+
def format_results(self, results: List[DocumentResult], query: str) -> None:
106+
if not results:
107+
typer.echo("No documents found matching the query.")
108+
return
109+
110+
typer.echo(f"━━━ Search Results ({len(results)} matches) ━━━")
111+
typer.echo()
112+
113+
for idx, doc in enumerate(results, 1):
114+
# Get file type icon and title
115+
icon = ModernCompactFormatter()._get_file_icon(doc.document.uri or "")
116+
ModernCompactFormatter()._get_document_title(doc.document.uri or "")
117+
118+
# Format metrics
119+
combined = (
120+
f"{doc.combined_rank:.5f}" if doc.combined_rank is not None else "N/A"
121+
)
122+
vec_info = (
123+
f"#{doc.vec_rank} ({doc.vec_distance:.6f})"
124+
if doc.vec_rank is not None
125+
else "N/A"
126+
)
127+
fts_info = (
128+
f"#{doc.fts_rank} ({doc.fts_score:.6f})"
129+
if doc.fts_rank is not None
130+
else "N/A"
131+
)
132+
133+
# Clean snippet
134+
snippet = self._clean_and_wrap_snippet(doc.snippet, width=75)
135+
136+
# Draw the result box
137+
typer.echo(f"┌─ Result #{idx} " + "─" * (67 - len(str(idx))))
138+
if doc.document.uri:
139+
uri_display = f"{icon} {doc.document.uri}"
140+
if len(uri_display) > 75:
141+
uri_display = f"{icon} ...{doc.document.uri[-70:]}"
142+
typer.echo(f"│ {uri_display:<75}│")
143+
typer.echo(f"│ Combined: {combined} │ Vector: {vec_info} │ FTS: {fts_info}")
144+
typer.echo("├" + "─" * 77 + "┤")
145+
146+
# Display snippet with proper wrapping
147+
for line in snippet:
148+
typer.echo(f"│ {line:<75} │")
149+
150+
typer.echo("└" + "─" * 77 + "┘")
151+
typer.echo()
152+
153+
def _clean_and_wrap_snippet(self, snippet: str, width: int = 75) -> List[str]:
154+
"""Clean snippet and wrap to specified width."""
155+
# Clean the snippet
156+
clean = snippet.replace("\n", " ").replace("\r", "")
157+
clean = " ".join(clean.split())
158+
159+
# Wrap to width
160+
lines = []
161+
words = clean.split()
162+
current_line = ""
163+
164+
for word in words:
165+
if len(current_line + " " + word) <= width:
166+
current_line = current_line + " " + word if current_line else word
167+
else:
168+
if current_line:
169+
lines.append(current_line)
170+
current_line = word
171+
172+
if current_line:
173+
lines.append(current_line)
174+
175+
# Limit to reasonable number of lines
176+
if len(lines) > 4:
177+
lines = lines[:3] + [lines[3][: width - 3] + "..."]
178+
179+
return lines
180+
181+
182+
class TableDebugFormatter(SearchResultFormatter):
183+
"""Legacy debug formatter for backwards compatibility."""
184+
185+
def format_results(self, results: List[DocumentResult], query: str) -> None:
186+
if not results:
187+
typer.echo("No documents found matching the query.")
188+
return
189+
190+
typer.echo(f"Found {len(results)} documents:")
191+
192+
# Enhanced debug table with better formatting
193+
typer.echo(
194+
f"{'#':<3} {'Preview':<55} {'URI':<35} {'C.Rank':<33} {'V.Rank':<8} {'FTS.Rank':<9} {'V.Dist':<18} {'FTS.Score':<18}"
195+
)
196+
typer.echo("─" * 180)
197+
198+
for idx, doc in enumerate(results, 1):
199+
# Clean snippet display
200+
snippet = doc.snippet.replace("\n", " ").replace("\r", "")
201+
if len(snippet) > 52:
202+
snippet = snippet[:49] + "..."
203+
204+
# Clean URI display
205+
uri = doc.document.uri or "N/A"
206+
if len(uri) > 32:
207+
uri = "..." + uri[-29:]
208+
209+
# Format debug values with proper precision
210+
c_rank = (
211+
f"{doc.combined_rank:.17f}" if doc.combined_rank is not None else "N/A"
212+
)
213+
v_rank = str(doc.vec_rank) if doc.vec_rank is not None else "N/A"
214+
fts_rank = str(doc.fts_rank) if doc.fts_rank is not None else "N/A"
215+
v_dist = (
216+
f"{doc.vec_distance:.6f}" if doc.vec_distance is not None else "N/A"
217+
)
218+
fts_score = f"{doc.fts_score:.6f}" if doc.fts_score is not None else "N/A"
219+
220+
typer.echo(
221+
f"{idx:<3} {snippet:<55} {uri:<35} {c_rank:<33} {v_rank:<8} {fts_rank:<9} {v_dist:<18} {fts_score:<18}"
222+
)
223+
224+
225+
class LegacyCompactFormatter(SearchResultFormatter):
226+
"""Legacy compact formatter for backwards compatibility."""
227+
228+
def format_results(self, results: List[DocumentResult], query: str) -> None:
229+
if not results:
230+
typer.echo("No documents found matching the query.")
231+
return
232+
233+
typer.echo(f"Found {len(results)} documents:")
234+
235+
# Clean simple table for normal view
236+
typer.echo(f"{'#':<3} {'Preview':<60} {'URI':<40}")
237+
typer.echo("─" * 105)
238+
239+
for idx, doc in enumerate(results, 1):
240+
# Clean snippet display
241+
snippet = doc.snippet.replace("\n", " ").replace("\r", "")
242+
if len(snippet) > 57:
243+
snippet = snippet[:54] + "..."
244+
245+
# Clean URI display
246+
uri = doc.document.uri or "N/A"
247+
if len(uri) > 37:
248+
uri = "..." + uri[-34:]
249+
250+
typer.echo(f"{idx:<3} {snippet:<60} {uri:<40}")
251+
252+
253+
def get_formatter(
254+
debug: bool = False, table_view: bool = False
255+
) -> SearchResultFormatter:
256+
"""Factory function to get the appropriate formatter."""
257+
if table_view:
258+
return TableDebugFormatter()
259+
elif debug:
260+
return ModernDetailedFormatter()
261+
else:
262+
return ModernCompactFormatter()

0 commit comments

Comments
 (0)