search method
Search for relevant chunks and assemble context for LLM.
adjacentChunks - Number of adjacent chunks to include before/after each
matched chunk (default: 0). Setting this to 1 will include the chunk
before and after each matched chunk, helping with long articles.
singleSourceMode - If true, only include chunks from the most relevant source.
Implementation
Future<RagSearchResult> search(
String query, {
int topK = 10,
int tokenBudget = 2000,
ContextStrategy strategy = ContextStrategy.relevanceFirst,
int adjacentChunks = 0,
bool singleSourceMode = false,
}) async {
// 1. Generate query embedding
final queryEmbedding = await EmbeddingService.embed(query);
// DEBUG: Log embedding stats
final embNorm = queryEmbedding.fold<double>(0, (sum, v) => sum + v * v);
print('[DEBUG] Query: "$query"');
print(
'[DEBUG] Embedding norm: ${embNorm.toStringAsFixed(4)}, dims: ${queryEmbedding.length}',
);
print(
'[DEBUG] First 5 values: ${queryEmbedding.take(5).map((v) => v.toStringAsFixed(4)).toList()}',
);
// 2. Search chunks
var chunks = await searchChunks(
dbPath: dbPath,
queryEmbedding: queryEmbedding,
topK: topK,
);
// 3. Filter to single source FIRST (before adjacent expansion)
// Pass the original query for text matching
if (singleSourceMode && chunks.isNotEmpty) {
chunks = _filterToMostRelevantSource(chunks, query);
}
// 4. Expand with adjacent chunks (only for the selected source)
if (adjacentChunks > 0 && chunks.isNotEmpty) {
chunks = await _expandWithAdjacentChunks(chunks, adjacentChunks);
}
// 5. Assemble context (pass singleSourceMode to skip headers when single source)
final context = ContextBuilder.build(
searchResults: chunks,
tokenBudget: tokenBudget,
strategy: strategy,
singleSourceMode: singleSourceMode, // Pass through to skip headers
);
return RagSearchResult(chunks: chunks, context: context);
}