mirror of
https://github.com/wenlzhang/obsidian-folder-navigator.git
synced 2026-07-22 05:41:23 +00:00
refactor: improve folder search by excluding redundant child matches instead of deprioritizing
This commit is contained in:
parent
89df8eb024
commit
658adcbfe4
2 changed files with 100 additions and 61 deletions
|
|
@ -9,8 +9,8 @@ This document describes the improvements made to the folder search functionality
|
|||
|
||||
**Solution**: Implemented intelligent parent-child deduplication that:
|
||||
- Prioritizes parent folder matches over their children
|
||||
- Deprioritizes child folders unless they have additional keyword matches beyond what the parent has
|
||||
- Reduces child folder scores to 10% of their original score when they don't add new information
|
||||
- Completely excludes child folders unless they have additional keyword matches beyond what the parent has
|
||||
- Only shows children when they contain additional matched keywords not found in the parent path
|
||||
|
||||
### Issue #8: Out-of-Order Keyword Matching
|
||||
**Problem**: Search only worked when keywords were in the correct order. For example, searching "bar foo" would not match "foo/bar/baz".
|
||||
|
|
@ -54,24 +54,36 @@ Each folder is scored based on multiple factors:
|
|||
After initial scoring, the algorithm applies deduplication:
|
||||
|
||||
```typescript
|
||||
// Check if this child has additional matches beyond the parent
|
||||
const parentKeywordMatches = keywords.filter(k =>
|
||||
parentPath.toLowerCase().includes(k)
|
||||
);
|
||||
const childKeywordMatches = keywords.filter(k =>
|
||||
folderPath.toLowerCase().includes(k)
|
||||
);
|
||||
// Count total occurrences of all keywords in both parent and child
|
||||
let parentOccurrences = 0;
|
||||
let childOccurrences = 0;
|
||||
|
||||
// If child doesn't have more keyword matches than parent, deprioritize it
|
||||
if (childKeywordMatches.length <= parentKeywordMatches.length) {
|
||||
item.score = item.score * 0.1;
|
||||
for (const keyword of keywords) {
|
||||
// Count occurrences of this keyword in parent
|
||||
let pos = 0;
|
||||
while ((pos = parentLower.indexOf(keyword, pos)) !== -1) {
|
||||
parentOccurrences++;
|
||||
pos += keyword.length;
|
||||
}
|
||||
|
||||
// Count occurrences of this keyword in child
|
||||
pos = 0;
|
||||
while ((pos = childLower.indexOf(keyword, pos)) !== -1) {
|
||||
childOccurrences++;
|
||||
pos += keyword.length;
|
||||
}
|
||||
}
|
||||
|
||||
// If child doesn't have more keyword occurrences than parent, exclude it
|
||||
if (childOccurrences <= parentOccurrences) {
|
||||
shouldExclude = true;
|
||||
}
|
||||
```
|
||||
|
||||
This ensures that when searching for "abc":
|
||||
- "abc" gets high priority
|
||||
- "abc/xxx" gets significantly lower priority (unless "xxx" is also a keyword)
|
||||
- "abc/xxx/abc" would still rank high because it has an additional "abc" match
|
||||
- "abc" is shown (1 occurrence)
|
||||
- "abc/xxx" is completely hidden (still 1 occurrence of "abc")
|
||||
- "abc/xxx/abc" would be shown (2 occurrences of "abc")
|
||||
|
||||
#### 4. Result Limiting
|
||||
|
||||
|
|
@ -101,20 +113,33 @@ Results are sorted by final score and limited to the `maxResults` setting.
|
|||
**After (new behavior):**
|
||||
1. `abc` (score: 350)
|
||||
2. `projects/abc` (score: 150)
|
||||
3. `abc/xxx` (score: 34) ← Deprioritized to 10%
|
||||
4. `abc/yyy` (score: 34) ← Deprioritized to 10%
|
||||
5. `abc/zzz` (score: 34) ← Deprioritized to 10%
|
||||
3. (Other non-related folders...)
|
||||
|
||||
Note: `abc/xxx`, `abc/yyy`, `abc/zzz` are completely excluded from results
|
||||
|
||||
### Example 3: Additional Keyword in Child
|
||||
**Query:** "abc test"
|
||||
|
||||
**Results:**
|
||||
1. `abc/test` (score: 450) - Matches both keywords, high score
|
||||
2. `abc` (score: 350) - Only matches "abc"
|
||||
3. `projects/abc/test` (score: 340) - Matches both but deeper
|
||||
4. `abc/notes` (score: 34) - Only matches "abc", child of matching parent
|
||||
1. `abc/test` (score: 450) - Matches both keywords, shown
|
||||
2. `abc` (score: 350) - Only matches "abc", shown
|
||||
3. `projects/abc/test` (score: 340) - Matches both, shown
|
||||
4. Other folders matching both keywords...
|
||||
|
||||
Note: `abc/test` ranks highest because it matches BOTH keywords even though it's a child of `abc`.
|
||||
Note: `abc/test` is shown because it has 2 total keyword occurrences ("abc" + "test"), more than its parent `abc` which only has 1. However, `abc/notes` would be excluded because it still only has 1 occurrence of "abc", the same count as its parent.
|
||||
|
||||
### Example 4: Repeated Keywords
|
||||
**Query:** "ai prompt"
|
||||
|
||||
**Folder Structure:**
|
||||
- `AI/Prompt engineering` - Contains "ai" (1x) and "prompt" (1x) = 2 total occurrences
|
||||
- `AI/Prompt engineering/Prompt engineering` - Contains "ai" (1x) and "prompt" (2x) = 3 total occurrences
|
||||
- `AI/Prompt engineering/asset` - Contains "ai" (1x) and "prompt" (1x) = 2 total occurrences
|
||||
|
||||
**Results:**
|
||||
1. `AI/Prompt engineering` - Shown (2 occurrences)
|
||||
2. `AI/Prompt engineering/Prompt engineering` - Shown (3 occurrences, more than parent)
|
||||
3. `AI/Prompt engineering/asset` - Hidden (2 occurrences, same as parent)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -288,13 +288,15 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
}
|
||||
|
||||
// Store match positions for highlighting (using original path positions)
|
||||
// We need to find the keyword in the original case-sensitive path
|
||||
// Find ALL occurrences of this keyword in the original case-sensitive path
|
||||
const originalPathLower = originalPath.toLowerCase();
|
||||
const matchIndex = originalPathLower.indexOf(keyword);
|
||||
|
||||
// Record character positions for this keyword
|
||||
for (let i = 0; i < keyword.length; i++) {
|
||||
matchPositions.push([matchIndex + i, matchIndex + i]);
|
||||
let pos = 0;
|
||||
while ((pos = originalPathLower.indexOf(keyword, pos)) !== -1) {
|
||||
// Record character positions for this occurrence
|
||||
for (let i = 0; i < keyword.length; i++) {
|
||||
matchPositions.push([pos + i, pos + i]);
|
||||
}
|
||||
pos += keyword.length; // Move past this occurrence to find the next one
|
||||
}
|
||||
|
||||
// Calculate score for this keyword
|
||||
|
|
@ -360,7 +362,7 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
});
|
||||
|
||||
// Apply parent folder deduplication for Issue #4
|
||||
// When a parent folder matches, deprioritize its immediate children
|
||||
// When a parent folder matches, completely exclude its children unless they have additional keyword matches
|
||||
const results: FuzzyMatch<TFolder>[] = [];
|
||||
const parentPaths = new Set<string>();
|
||||
|
||||
|
|
@ -374,7 +376,7 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
const folderPath = item.folder.path;
|
||||
|
||||
// Check if this folder's parent is already in results
|
||||
let shouldInclude = true;
|
||||
let shouldExclude = false;
|
||||
let isChildOfMatch = false;
|
||||
|
||||
for (const parentPath of parentPaths) {
|
||||
|
|
@ -382,48 +384,60 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
if (folderPath.startsWith(parentPath + "/")) {
|
||||
isChildOfMatch = true;
|
||||
|
||||
// Check if this child has additional matches beyond the parent
|
||||
const parentKeywordMatches = keywords.filter((k) =>
|
||||
parentPath.toLowerCase().includes(k),
|
||||
);
|
||||
const childKeywordMatches = keywords.filter((k) =>
|
||||
folderPath.toLowerCase().includes(k),
|
||||
);
|
||||
// Check if this child has additional keyword occurrences beyond the parent
|
||||
// Count total occurrences of all keywords in both parent and child
|
||||
const parentLower = parentPath.toLowerCase();
|
||||
const childLower = folderPath.toLowerCase();
|
||||
|
||||
// If child doesn't have more keyword matches than parent, deprioritize it
|
||||
if (
|
||||
childKeywordMatches.length <=
|
||||
parentKeywordMatches.length
|
||||
) {
|
||||
// Lower the score significantly to push it down
|
||||
item.score = item.score * 0.1;
|
||||
item.match.match.score = item.score;
|
||||
let parentOccurrences = 0;
|
||||
let childOccurrences = 0;
|
||||
|
||||
for (const keyword of keywords) {
|
||||
// Count occurrences of this keyword in parent
|
||||
let pos = 0;
|
||||
while (
|
||||
(pos = parentLower.indexOf(keyword, pos)) !== -1
|
||||
) {
|
||||
parentOccurrences++;
|
||||
pos += keyword.length;
|
||||
}
|
||||
|
||||
// Count occurrences of this keyword in child
|
||||
pos = 0;
|
||||
while (
|
||||
(pos = childLower.indexOf(keyword, pos)) !== -1
|
||||
) {
|
||||
childOccurrences++;
|
||||
pos += keyword.length;
|
||||
}
|
||||
}
|
||||
|
||||
// If child doesn't have more keyword occurrences than parent, exclude it
|
||||
if (childOccurrences <= parentOccurrences) {
|
||||
shouldExclude = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
results.push(item.match);
|
||||
// Only include if not excluded
|
||||
if (!shouldExclude) {
|
||||
results.push(item.match);
|
||||
|
||||
// Add this folder to parent paths for future checks
|
||||
if (!isChildOfMatch) {
|
||||
parentPaths.add(folderPath);
|
||||
// Add this folder to parent paths for future checks
|
||||
if (!isChildOfMatch) {
|
||||
parentPaths.add(folderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort after deduplication adjustments
|
||||
const finalResults = results.sort((a, b) => {
|
||||
// Keep separators at original positions
|
||||
if ((a.item as any)._isSeparator || (b.item as any)._isSeparator) {
|
||||
return 0;
|
||||
}
|
||||
return (b.match?.score || 0) - (a.match?.score || 0);
|
||||
});
|
||||
|
||||
// Limit results
|
||||
const limitedResults = finalResults.slice(0, this.limit);
|
||||
const limitedResults = results.slice(0, this.limit);
|
||||
|
||||
log(`Found ${limitedResults.length} matching folders`, this.plugin);
|
||||
log(
|
||||
`Found ${limitedResults.length} matching folders (filtered from ${foldersWithScore.length} total matches)`,
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
return limitedResults;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue