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
Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,75 @@ public IActionResult Search([FromQuery] string framework, [FromQuery] string que
if (string.IsNullOrWhiteSpace(query))
return Content("Empty query.", "text/plain");

// Field-weighted re-ranking: FTS4 BM25 treats all columns equally and ranks
// by raw term frequency across the corpus, causing dedicated feature docs to
// rank below generic docs that mention the same terms in passing.
// We fix this by computing a field-boost score: a term match in the doc
// title (toc_name) is worth far more than a match buried in body text.
// This generalizes to any query — no per-feature heuristics needed.
var terms = query
Comment thread
onlyexeption marked this conversation as resolved.
.Replace("*", " ").Replace("\"", " ")
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Where(t => t.Length > 2)
.Select(t => t.ToLowerInvariant())
.ToList();
Comment thread
onlyexeption marked this conversation as resolved.

var cmd = db.CreateCommand();
cmd.CommandText = @"
SELECT d.framework, d.filename, d.component, d.toc_name, d.premium,
SELECT d.filename, d.component, d.toc_name, d.keywords, d.summary,
snippet(docs_fts, '>>>', '<<<', '...', -1, 32) AS excerpt
FROM docs_fts JOIN docs d ON d.rowid = docs_fts.rowid
WHERE docs_fts MATCH @q AND d.framework = @fw
LIMIT 20";
LIMIT 200";
cmd.Parameters.AddWithValue("@q", query);
cmd.Parameters.AddWithValue("@fw", framework);

var sb = new StringBuilder();
var results = new List<(int score, int idx, string filename, string? component, string? tocName, string? summary, string excerpt)>();
var rowIdx = 0;
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var filename = reader.GetString(reader.GetOrdinal("filename"));
var docName = filename.EndsWith(".md") ? filename[..^3] : filename;
var component = reader.IsDBNull(reader.GetOrdinal("component")) ? null : reader.GetString(reader.GetOrdinal("component"));
var comp = !string.IsNullOrEmpty(component) ? $" [{component}]" : "";
var excerpt = reader.GetString(reader.GetOrdinal("excerpt"));
var tocName = reader.IsDBNull(reader.GetOrdinal("toc_name")) ? null : reader.GetString(reader.GetOrdinal("toc_name"));
var keywords = reader.IsDBNull(reader.GetOrdinal("keywords")) ? null : reader.GetString(reader.GetOrdinal("keywords"));
var summary = reader.IsDBNull(reader.GetOrdinal("summary")) ? null : reader.GetString(reader.GetOrdinal("summary"));
var excerpt = reader.GetString(reader.GetOrdinal("excerpt"));

var toc = (tocName ?? "").ToLowerInvariant();
var fn = filename.ToLowerInvariant();
var kw = (keywords ?? "").ToLowerInvariant();
var sm = (summary ?? "").ToLowerInvariant();

var score = 0;
foreach (var t in terms)
{
if (toc.Contains(t)) score += 10; // title match — strongest signal
if (fn.Contains(t)) score += 4; // filename contains the concept
if (kw.Contains(t)) score += 5; // curated keyword metadata
if (sm.Contains(t)) score += 3; // summary mentions the concept
// content matches are already captured by FTS; no extra boost needed
}

results.Add((score, rowIdx++, filename, component, tocName, summary, excerpt));
}

var ranked = results
.OrderByDescending(r => r.score)
.ThenBy(r => r.idx)
.Take(20);

var sb = new StringBuilder();
foreach (var r in ranked)
{
var docName = r.filename.EndsWith(".md") ? r.filename[..^3] : r.filename;
var comp = !string.IsNullOrEmpty(r.component) ? $" [{r.component}]" : "";
if (sb.Length > 0) sb.AppendLine().AppendLine();
sb.Append($"**{docName}**{comp}\n{excerpt}");
sb.Append($"**{docName}**{comp}\n{r.excerpt}");
}

var text = sb.Length > 0 ? sb.ToString() : $"No results found for \"{query}\".";
var displayQuery = System.Text.RegularExpressions.Regex.Replace(query.Replace('"', ' ').Replace('*', ' '), @"\s+", " ").Trim();
var text = sb.Length > 0 ? sb.ToString() : $"No results found for \"{displayQuery}\".";
return Content(text, "text/plain");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,56 @@ public void Search_OnlyReturnsRequestedFramework()
Assert.That(result.Content, Does.Not.Contain("grid-editing"));
Assert.That(result.Content, Does.Not.Contain("grid-filtering"));
}

[Test]
public void Search_TitleAndKeywordMatchesOutrankBodyOnlyMatch()
{
// Isolated DB: one doc has the query term in toc_name + keywords (dedicated feature doc),
// the other only mentions it once in the body (generic doc that references it in passing).
// Verifies that field-weighted re-ranking surfaces the dedicated doc first.
using var rankDb = new SqliteConnection("Data Source=:memory:");
rankDb.Open();

void Exec(string sql) { var c = rankDb.CreateCommand(); c.CommandText = sql; c.ExecuteNonQuery(); }

Exec(@"CREATE TABLE docs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
framework TEXT NOT NULL, filename TEXT NOT NULL, component TEXT NOT NULL,
toc_name TEXT, premium INTEGER DEFAULT 0,
keywords TEXT, summary TEXT, content TEXT NOT NULL,
UNIQUE(framework, filename))");

Exec(@"CREATE VIRTUAL TABLE docs_fts USING fts4(
component, toc_name, keywords, summary, content,
content='docs', tokenize=porter)");

Exec(@"INSERT INTO docs (framework, filename, component, toc_name, premium, keywords, summary, content)
VALUES ('angular', 'grid-virtualization.md', 'IgxGridComponent',
'Virtualization and Performance', 0,
'virtualization, performance, scrolling',
'How grid virtualization works.',
'Virtualization improves rendering performance.')");

Exec(@"INSERT INTO docs (framework, filename, component, toc_name, premium, keywords, summary, content)
VALUES ('angular', 'grid-editing.md', 'IgxGridComponent',
'Grid Editing', 0,
'editing, cell',
'How to edit grid cells.',
'The grid uses virtualization internally for performance.')");

Exec("INSERT INTO docs_fts (rowid, component, toc_name, keywords, summary, content) SELECT id, component, toc_name, keywords, summary, content FROM docs");

var controller = new DocsController(rankDb);
var result = controller.Search("angular", "\"virtualization\"") as ContentResult;

Assert.That(result, Is.Not.Null);
var content = result!.Content!;
var dedicatedPos = content.IndexOf("**grid-virtualization**", StringComparison.Ordinal);
var genericPos = content.IndexOf("**grid-editing**", StringComparison.Ordinal);

Assert.That(dedicatedPos, Is.GreaterThanOrEqualTo(0), "grid-virtualization should appear in results");
Assert.That(genericPos, Is.GreaterThanOrEqualTo(0), "grid-editing should appear in results");
Assert.That(dedicatedPos, Is.LessThan(genericPos),
"grid-virtualization (title + keyword match) should rank before grid-editing (body-only match)");
}
}
Binary file modified packages/igniteui-mcp/igniteui-doc-mcp/db/igniteui-docs.db
Binary file not shown.
Loading