Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions brain-bar/Sources/BrainBar/BrainDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,16 @@ final class BrainDatabase: @unchecked Sendable {
if importanceMin != nil { conditions.append("c.importance >= ?") }
if unreadOnly { conditions.append("c.rowid > ?") }

// Unread mode needs contiguous rowid ordering for watermark semantics.
// Normal search uses FTS5 BM25 rank for relevance ordering.
let orderByClause = unreadOnly ? "c.rowid ASC" : "f.rank"
let sql = """
SELECT c.rowid, c.id, c.content, c.project, c.content_type, c.importance,
c.created_at, c.summary, c.tags, c.conversation_id
c.created_at, c.summary, c.tags, c.conversation_id, f.rank
FROM chunks_fts f
JOIN chunks c ON c.id = f.chunk_id
WHERE \(conditions.joined(separator: " AND "))
ORDER BY c.rowid ASC
ORDER BY \(orderByClause)
LIMIT ?
"""

Expand Down Expand Up @@ -274,6 +277,9 @@ final class BrainDatabase: @unchecked Sendable {
while sqlite3_step(stmt) == SQLITE_ROW {
let rowID = sqlite3_column_int64(stmt, 0)
maxRowID = max(maxRowID, rowID)
// FTS5 rank is negative (lower = better match). Negate for a positive score.
let rawRank = sqlite3_column_double(stmt, 10)
let score = max(0, -rawRank)
results.append([
"rowid": Int(rowID),
"chunk_id": columnText(stmt, 1) as Any,
Expand All @@ -284,7 +290,8 @@ final class BrainDatabase: @unchecked Sendable {
"created_at": columnText(stmt, 6) as Any,
"summary": columnText(stmt, 7) as Any,
"tags": columnText(stmt, 8) as Any,
"session_id": columnText(stmt, 9) as Any
"session_id": columnText(stmt, 9) as Any,
"score": score
])
}

Expand Down
7 changes: 5 additions & 2 deletions brain-bar/Sources/BrainBar/Formatters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ enum Formatters {
let importance = r["importance"]
let summary = r["summary"] as? String ?? ""
let content = r["content"] as? String ?? ""
let displayText = truncate(summary.isEmpty ? content : summary, maxLen: 72)
let displayText = truncate(summary.isEmpty ? content : summary, maxLen: 150)

let impStr: String
if let imp = importance as? Double {
Expand All @@ -129,7 +129,10 @@ enum Formatters {
let tagStr = tags.prefix(4).joined(separator: ", ")
lines.append("\u{2502} \(key("tags:", useColor)) \(tagStr)")
}
lines.append("\u{2502}")
// Separator between results, but not after the last one
if i < results.count - 1 {
lines.append("\u{2502}")
}
}

lines.append("\u{2514}\u{2500}")
Expand Down
23 changes: 23 additions & 0 deletions brain-bar/Tests/BrainBarTests/DatabaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,27 @@ final class DatabaseTests: XCTestCase {

wait(for: [expectation], timeout: 5.0)
}

// MARK: - Search Ranking (FTS5 BM25)

func testSearchResultsHaveNonZeroScore() throws {
try db.insertChunk(id: "score-1", content: "BrainBar Swift daemon formatting", sessionId: "s1", project: "test", contentType: "assistant_text", importance: 5)

let results = try db.search(query: "BrainBar daemon", limit: 5)
XCTAssertFalse(results.isEmpty)
let score = results.first?["score"] as? Double ?? 0
XCTAssertGreaterThan(score, 0, "Search results should have a non-zero relevance score")
}

func testSearchResultsOrderedByRelevance() throws {
// "sprint" appears in content of both, but the first has it more prominently
try db.insertChunk(id: "rel-1", content: "The overnight hardening sprint was a success. Sprint results show improvements.", sessionId: "s1", project: "test", contentType: "assistant_text", importance: 5)
try db.insertChunk(id: "rel-2", content: "We discussed various topics including weather and sprint planning briefly.", sessionId: "s2", project: "test", contentType: "assistant_text", importance: 5)

let results = try db.search(query: "sprint", limit: 5)
XCTAssertEqual(results.count, 2)
let score1 = results[0]["score"] as? Double ?? 0
let score2 = results[1]["score"] as? Double ?? 0
XCTAssertGreaterThanOrEqual(score1, score2, "Results should be ordered by relevance (highest score first)")
}
}
42 changes: 42 additions & 0 deletions brain-bar/Tests/BrainBarTests/FormattersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,46 @@ final class FormattersTests: XCTestCase {
XCTAssertTrue(entityOut.contains("\u{250c}"))
XCTAssertTrue(entityOut.contains("\u{2514}"))
}

// MARK: - Layout: No trailing empty │ lines

func testSearchResultsNoTrailingEmptyLine() {
let result: [String: Any] = [
"chunk_id": "x1", "score": 0.5, "project": "test",
"created_at": "2026-01-01", "summary": "Some result", "importance": 5
]
let out = Formatters.formatSearchResults(query: "q", results: [result], total: 1, useColor: false)
let lines = out.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
// The line before └─ should NOT be a bare │
let closerIdx = lines.lastIndex(where: { $0.hasPrefix("\u{2514}") })!
let beforeCloser = lines[closerIdx - 1]
XCTAssertNotEqual(beforeCloser, "\u{2502}", "Should not have a trailing empty │ line before └─")
}

func testSearchResultsMultipleNoTrailingGap() {
let results: [[String: Any]] = [
["chunk_id": "a", "score": 0.9, "project": "p", "created_at": "d", "summary": "First", "importance": 7],
["chunk_id": "b", "score": 0.5, "project": "p", "created_at": "d", "summary": "Second", "importance": 3],
]
let out = Formatters.formatSearchResults(query: "q", results: results, total: 2, useColor: false)
let lines = out.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
let closerIdx = lines.lastIndex(where: { $0.hasPrefix("\u{2514}") })!
let beforeCloser = lines[closerIdx - 1]
XCTAssertNotEqual(beforeCloser, "\u{2502}", "Last result should not have trailing │ gap")
}

// MARK: - Content truncation: 150 chars

func testSearchResultsSummaryTruncatesAt150() {
let longSummary = String(repeating: "x", count: 200)
let result: [String: Any] = [
"chunk_id": "t1", "score": 0.5, "project": "test",
"created_at": "2026-01-01", "summary": longSummary, "importance": 5
]
let out = Formatters.formatSearchResults(query: "q", results: [result], total: 1, useColor: false)
// Exact truncation: 149 chars + ellipsis = 150 total
let expected = String(repeating: "x", count: 149) + "\u{2026}"
XCTAssertTrue(out.contains(expected), "Summary should truncate to 149 chars + ellipsis (150 total)")
XCTAssertFalse(out.contains(longSummary), "200-char summary should be truncated")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading