-
Notifications
You must be signed in to change notification settings - Fork 125
Refactor search ranking #3424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor search ranking #3424
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | ||
// for details. All rights reserved. Use of this source code is governed by a | ||
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'dart:convert'; | ||
|
||
import 'package:dartdoc/src/generator/generator_utils.dart'; | ||
import 'package:meta/meta.dart'; | ||
|
||
enum _MatchPosition { | ||
isExactly, | ||
startsWith, | ||
contains; | ||
|
||
int operator -(_MatchPosition other) => index - other.index; | ||
} | ||
|
||
class Index { | ||
final List<String> packageOrder; | ||
final List<IndexItem> index; | ||
|
||
@visibleForTesting | ||
Index(this.packageOrder, this.index); | ||
|
||
factory Index.fromJson(String text) { | ||
var jsonIndex = (jsonDecode(text) as List).cast<Map<String, Object>>(); | ||
var indexList = <IndexItem>[]; | ||
var packageOrder = <String>[]; | ||
for (var entry in jsonIndex) { | ||
if (entry.containsKey(packageOrderKey)) { | ||
packageOrder.addAll(entry[packageOrderKey] as List<String>); | ||
} else { | ||
indexList.add(IndexItem.fromMap(entry)); | ||
} | ||
} | ||
return Index(packageOrder, indexList); | ||
} | ||
|
||
int packageOrderPosition(String packageName) { | ||
if (packageOrder.isEmpty) return 0; | ||
var index = packageOrder.indexOf(packageName); | ||
return index == -1 ? packageOrder.length : index; | ||
} | ||
|
||
List<IndexItem> find(String rawQuery) { | ||
if (rawQuery.isEmpty) { | ||
return []; | ||
} | ||
|
||
var query = rawQuery.toLowerCase(); | ||
var allMatches = <({IndexItem item, _MatchPosition matchPosition})>[]; | ||
|
||
for (var item in index) { | ||
void score(_MatchPosition matchPosition) { | ||
allMatches.add((item: item, matchPosition: matchPosition)); | ||
} | ||
|
||
var lowerName = item.name.toLowerCase(); | ||
var lowerQualifiedName = item.qualifiedName.toLowerCase(); | ||
|
||
if (lowerName == query || | ||
lowerQualifiedName == query || | ||
lowerName == 'dart:$query') { | ||
score(_MatchPosition.isExactly); | ||
} else if (query.length > 1) { | ||
if (lowerName.startsWith(query) || | ||
lowerQualifiedName.startsWith(query)) { | ||
score(_MatchPosition.startsWith); | ||
} else if (lowerName.contains(query) || | ||
lowerQualifiedName.contains(query)) { | ||
score(_MatchPosition.contains); | ||
} | ||
} | ||
} | ||
|
||
allMatches.sort((a, b) { | ||
// Exact match vs substring is king. If the user has typed the whole term | ||
// they are searching for, but it isn't at the top, they cannot type any | ||
// more to try and find it. | ||
var comparison = a.matchPosition - b.matchPosition; | ||
if (comparison != 0) { | ||
return comparison; | ||
} | ||
|
||
// Prefer packages higher in the package order. | ||
comparison = packageOrderPosition(a.item.packageName) - | ||
packageOrderPosition(b.item.packageName); | ||
if (comparison != 0) { | ||
return comparison; | ||
} | ||
|
||
// Prefer top-level elements to library members to class (etc.) members. | ||
comparison = a.item._scope - b.item._scope; | ||
if (comparison != 0) { | ||
return comparison; | ||
} | ||
|
||
// Prefer non-overrides to overrides. | ||
comparison = a.item.overriddenDepth - b.item.overriddenDepth; | ||
if (comparison != 0) { | ||
return comparison; | ||
} | ||
|
||
// Prefer shorter names to longer ones. | ||
return a.item.name.length - b.item.name.length; | ||
}); | ||
|
||
return allMatches.map((match) => match.item).toList(); | ||
} | ||
} | ||
|
||
class IndexItem { | ||
final String name; | ||
final String qualifiedName; | ||
|
||
// TODO(srawlins): Store the integer of in the package order instead of this | ||
// String. The Strings bloat the `index.json` file and keeping duplicate | ||
// parsed Strings in memory is expensive. | ||
final String packageName; | ||
final String type; | ||
final String? href; | ||
final int overriddenDepth; | ||
final String? desc; | ||
final EnclosedBy? enclosedBy; | ||
|
||
IndexItem._({ | ||
required this.name, | ||
required this.qualifiedName, | ||
required this.packageName, | ||
required this.type, | ||
required this.desc, | ||
required this.href, | ||
required this.overriddenDepth, | ||
required this.enclosedBy, | ||
}); | ||
|
||
// Example Map structure: | ||
// | ||
// ```dart | ||
// { | ||
// "name":"dartdoc", | ||
// "qualifiedName":"dartdoc", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: example as "libName.variableName" might be better? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
// "href":"dartdoc/dartdoc-library.html", | ||
// "type":"library", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this might be a field that could be crushed into an integer too, if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it would be nice. We actually display the "type" in the search result so we would have to store the "type" as an int (rather convert to a scope), but yes still savings. |
||
// "overriddenDepth":0, | ||
// "packageName":"dartdoc" | ||
// ["enclosedBy":{"name":"Accessor","type":"class"}] | ||
// } | ||
// ``` | ||
factory IndexItem.fromMap(Map<String, dynamic> data) { | ||
// Note that this map also contains 'packageName', but we're not currently | ||
// using that info. | ||
|
||
EnclosedBy? enclosedBy; | ||
if (data['enclosedBy'] != null) { | ||
final map = data['enclosedBy'] as Map<String, Object>; | ||
enclosedBy = EnclosedBy._( | ||
name: map['name'] as String, | ||
type: map['type'] as String, | ||
href: map['href'] as String); | ||
} | ||
|
||
return IndexItem._( | ||
name: data['name'], | ||
qualifiedName: data['qualifiedName'], | ||
packageName: data['packageName'], | ||
href: data['href'], | ||
type: data['type'], | ||
overriddenDepth: (data['overriddenDepth'] as int?) ?? 0, | ||
desc: data['desc'], | ||
enclosedBy: enclosedBy, | ||
); | ||
} | ||
|
||
int get _scope => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A comment explaining this would be nice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, done! And added a TODO, as its not well-tested. |
||
const { | ||
'topic': 0, | ||
'library': 0, | ||
'class': 1, | ||
'enum': 1, | ||
'mixin': 1, | ||
'extension': 1, | ||
'typedef': 1, | ||
'function': 2, | ||
'method': 2, | ||
'accessor': 2, | ||
'operator': 2, | ||
'constant': 2, | ||
'property': 2, | ||
'constructor': 2, | ||
}[type] ?? | ||
3; | ||
} | ||
|
||
class EnclosedBy { | ||
final String name; | ||
final String type; | ||
final String href; | ||
|
||
// Built from JSON structure: | ||
// ["enclosedBy":{"name":"Accessor","type":"class","href":"link"}] | ||
EnclosedBy._({required this.name, required this.type, required this.href}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
the index of the package in package order
or similarThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Done