Skip to content

Commit 4031e33

Browse files
committed
Optimized & added features to search
1 parent 4b11ad4 commit 4031e33

File tree

5 files changed

+197
-198
lines changed

5 files changed

+197
-198
lines changed

scaladoc-js/main/src/searchbar/SearchbarComponent.scala

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dotty.tools.scaladoc
22

3+
import scala.concurrent.{ Future, ExecutionContext }
4+
import concurrent.ExecutionContext.Implicits.global
35
import utils.HTML._
46

57
import scala.scalajs.js.Date
@@ -92,54 +94,67 @@ class SearchbarComponent(engine: PageSearchEngine, inkuireEngine: InkuireJSSearc
9294
span(kind)
9395
)
9496
def handleNewFluffQuery(query: NameAndKindQuery) =
95-
val result: List[MatchResult] = engine.query(query)
96-
val fragment = document.createDocumentFragment()
97-
def createLoadMoreElement =
98-
div(cls := "scaladoc-searchbar-row mono-small-inline", "loadmore" := "")(
99-
a(
100-
span("Load more")
101-
)
102-
).tap { loadMoreElement => loadMoreElement
103-
.addEventListener("mouseover", _ => handleHover(loadMoreElement))
97+
val searchTask: Future[List[MatchResult]] = engine.query(query)
98+
searchTask.map { result =>
99+
val resultWithDocBonus = result
100+
.map(entry =>
101+
// add bonus score for static pages when in documentation section
102+
if entry.pageEntry.kind == "static" && !window.location.href.contains("api") then
103+
entry.copy(score = entry.score + 7)
104+
else entry
105+
)
106+
val fragment = document.createDocumentFragment()
107+
108+
def createLoadMoreElement =
109+
div(cls := "scaladoc-searchbar-row mono-small-inline", "loadmore" := "")(
110+
a(
111+
span("Load more")
112+
)
113+
).tap { loadMoreElement =>
114+
loadMoreElement
115+
.addEventListener("mouseover", _ => handleHover(loadMoreElement))
116+
}
117+
118+
val groupedResults = resultWithDocBonus.groupBy(_.pageEntry.kind)
119+
val groupedResultsSortedByScore = groupedResults.map {
120+
case (kind, results) => (kind, results.maxByOption(_.score).map(_.score), results)
121+
}.toList.sortBy {
122+
case (_, topScore, _) => -topScore.getOrElse(0)
123+
}.map {
124+
case (kind, _, results) => (kind, results.take(40)) // limit to 40 results per category
104125
}
105-
val groupedResults = result.groupBy(_.pageEntry.kind)
106-
val groupedResultsSortedByScore = groupedResults.map {
107-
case (kind, results) => (kind, results.maxByOption(_.score).map(_.score), results)
108-
}.toList.sortBy {
109-
case (kind, topScore, results) => -topScore.getOrElse(0)
110-
}.map {
111-
case (kind, topScore, results) => (kind, results)
112-
}
113126

114-
groupedResultsSortedByScore.map {
115-
case (kind, results) =>
116-
val kindSeparator = createKindSeparator(kind)
117-
val htmlEntries = results.map(result => result.pageEntry.toHTML(result.indices))
118-
val loadMoreElement = createLoadMoreElement
119-
def loadMoreResults(entries: List[raw.HTMLElement]): Unit = {
120-
loadMoreElement.onclick = (event: Event) => {
121-
entries.take(resultsChunkSize).foreach(_.classList.remove("hidden"))
122-
val nextElems = entries.drop(resultsChunkSize)
123-
if nextElems.nonEmpty then loadMoreResults(nextElems) else loadMoreElement.classList.add("hidden")
127+
groupedResultsSortedByScore.map {
128+
case (kind, results) =>
129+
val kindSeparator = createKindSeparator(kind)
130+
val htmlEntries = results.map(result => result.pageEntry.toHTML(result.indices))
131+
val loadMoreElement = createLoadMoreElement
132+
133+
def loadMoreResults(entries: List[raw.HTMLElement]): Unit = {
134+
loadMoreElement.onclick = (event: Event) => {
135+
entries.take(resultsChunkSize).foreach(_.classList.remove("hidden"))
136+
val nextElems = entries.drop(resultsChunkSize)
137+
if nextElems.nonEmpty then loadMoreResults(nextElems) else loadMoreElement.classList.add("hidden")
138+
}
124139
}
125-
}
126140

127-
fragment.appendChild(kindSeparator)
128-
htmlEntries.foreach(fragment.appendChild)
129-
fragment.appendChild(loadMoreElement)
141+
fragment.appendChild(kindSeparator)
142+
htmlEntries.foreach(fragment.appendChild)
143+
fragment.appendChild(loadMoreElement)
130144

131-
val nextElems = htmlEntries.drop(initialChunkSize)
132-
if nextElems.nonEmpty then {
133-
nextElems.foreach(_.classList.add("hidden"))
134-
loadMoreResults(nextElems)
135-
} else {
136-
loadMoreElement.classList.add("hidden")
137-
}
145+
val nextElems = htmlEntries.drop(initialChunkSize)
146+
if nextElems.nonEmpty then {
147+
nextElems.foreach(_.classList.add("hidden"))
148+
loadMoreResults(nextElems)
149+
} else {
150+
loadMoreElement.classList.add("hidden")
151+
}
138152

139-
}
153+
}
140154

141-
resultsDiv.scrollTop = 0
142-
resultsDiv.appendChild(fragment)
155+
resultsDiv.scrollTop = 0
156+
resultsDiv.appendChild(fragment)
157+
}
143158

144159
def handleRecentQueries(query: String) = {
145160
val recentQueries = RecentQueryStorage.getData

scaladoc-js/main/src/searchbar/engine/PageSearchEngine.scala

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dotty.tools.scaladoc
22

3+
import scala.concurrent.{ Future, ExecutionContext }
4+
import concurrent.ExecutionContext.Implicits.global
35
import math.Ordering.Implicits.seqOrdering
46
import org.scalajs.dom.Node
57

@@ -16,24 +18,28 @@ import scala.annotation.tailrec
1618
* - Optimize.
1719
*/
1820
class PageSearchEngine(pages: List[PageEntry]):
19-
def query(query: NameAndKindQuery): List[MatchResult] =
20-
println("QUERYING: " + query)
21+
22+
def query(query: NameAndKindQuery): Future[List[MatchResult]] = Future {
23+
val time = System.currentTimeMillis()
2124
matchPages(query)
2225
.filter {
2326
case MatchResult(score, _, _) => score >= 0
2427
}
2528
.sortBy {
2629
case MatchResult(score, _, _) => -score
2730
}
31+
}
2832

2933
private def kindScoreBonus(kind: String): Int = kind match {
3034
case "class" => 5
31-
case "object" | "enu," => 4
35+
case "object" | "enum" => 4
3236
case "trait" => 3
3337
case "def" | "val" | "given" | "type" => 1
3438
case _ => 0
3539
}
3640

41+
private val positionScores = List(8,4,2,1).orElse(PartialFunction.fromFunction(_ => 0))
42+
3743
private def matchCompletnessBonus(nameCharacters: Int, matchCharacters: Int): Int =
3844
(matchCharacters * 3) / nameCharacters
3945

@@ -47,53 +53,45 @@ class PageSearchEngine(pages: List[PageEntry]):
4753
.map(MatchResult(1, _, Set.empty))
4854
case NameAndKindQuery(Some(nameSearch), kind) =>
4955
val kindFiltered = kind.fold(pages)(filterKind(pages, _))
50-
println("PREMATCHING: " + nameSearch)
5156
val prematchedPages = prematchPages(kindFiltered, nameSearch)
52-
println("PREMATCHED " + prematchedPages.length)
53-
var totalMatched = 0
5457

55-
if nameSearch.length > 2 then
56-
prematchedPages.map(prematched =>
58+
if nameSearch.length > 1 then
59+
prematchedPages.map { prematched =>
5760
val finalMatch = matchPage(prematched, nameSearch)
5861
val bonusScore = kindScoreBonus(prematched.pageEntry.kind)
5962
+ matchCompletnessBonus(prematched.pageEntry.shortName.length, nameSearch.length)
6063
finalMatch.copy(score = finalMatch.score + bonusScore)
61-
)
64+
}
6265
else prematchedPages
6366

6467

6568
private def filterKind(pages: List[PageEntry], kind: String): List[PageEntry] =
6669
pages.filter(_.kind == kind)
6770

6871
def prematchPages(pages: List[PageEntry], search: String): List[MatchResult] =
69-
pages.map(page => MatchResult(1, page, getIndicesOfSearchLetters(page.shortName, search)))
70-
.filter(_.indices.nonEmpty)
72+
pages.map(prematchPage(_, search)).filter(_.indices.nonEmpty)
7173

72-
val debuggedTypes = Set("list", "lazylist", "classmanifest")
73-
74-
private def getIndicesOfSearchLetters(pageName: String, search: String): Set[Int] =
75-
if debuggedTypes.contains(pageName) then println("Prematching List!")
74+
private def prematchPage(page: PageEntry, search: String): MatchResult =
75+
val pageName = page.shortName
7676
@tailrec
77-
def getIndicesAcc(nameIndex: Int, searchIndex: Int, acc: Set[Int]): Set[Int] =
77+
def prematchPageAcc(nameIndex: Int, searchIndex: Int, acc: Set[Int], scoreAcc: Int, consecutiveMatches: Int): MatchResult =
7878
if searchIndex >= search.length then
79-
if debuggedTypes.contains(pageName) then println("Matched!")
80-
acc
79+
MatchResult(scoreAcc, page, acc)
8180
else if nameIndex >= pageName.length then
82-
if debuggedTypes.contains(pageName) then println("Not matched :(")
83-
Set.empty
81+
MatchResult(0, page, Set.empty)
8482
else if pageName(nameIndex).toLower == search(searchIndex).toLower then
85-
if debuggedTypes.contains(pageName) then println("Matched name:" + nameIndex + "(" + pageName(nameIndex) + ") with search:" + searchIndex + "(" + search(searchIndex) + ")")
86-
getIndicesAcc(nameIndex + 1, searchIndex + 1, acc + nameIndex)
83+
val score = (if consecutiveMatches > 0 then 1 else 0) + positionScores(nameIndex)
84+
prematchPageAcc(nameIndex + 1, searchIndex + 1, acc + nameIndex, scoreAcc + score, consecutiveMatches + 1)
8785
else
88-
if debuggedTypes.contains(pageName) then println("Not matched: " + nameIndex + "(" + pageName(nameIndex) + ") with search:" + searchIndex + "(" + search(searchIndex) + ")")
89-
getIndicesAcc(nameIndex + 1, searchIndex, acc)
90-
getIndicesAcc(0, 0, Set.empty)
86+
prematchPageAcc(nameIndex + 1, searchIndex, acc, scoreAcc, 0)
87+
88+
val result = prematchPageAcc(0, 0, Set.empty, 0, 0)
89+
result.copy(score = result.score + kindScoreBonus(page.kind))
9190

9291
private def matchPage(prematched: MatchResult, nameSearch: String): MatchResult =
9392
val searchTokens: List[List[Char]] = StringUtils.createCamelCaseTokens(nameSearch).map(_.toList) //todo extract
9493
val pageTokens: List[List[Char]] = prematched.pageEntry.tokens.map(_.toList)
9594
val pageName = prematched.pageEntry.shortName
96-
if debuggedTypes.contains(pageName) then println("Found " + pageName + "!")
9795
val searchTokensLifted = searchTokens.lift
9896
val pageTokensLifted = pageTokens.lift
9997

@@ -105,7 +103,7 @@ class PageSearchEngine(pages: List[PageEntry]):
105103
if searchHead == pageHead then
106104
matchTokens(searchTokenIndex + 1, pageTokenIndex + 1, acc + ((searchTokenIndex, pageTokenIndex)))
107105
else
108-
matchTokens(searchTokenIndex + 1, pageTokenIndex + 1, acc)
106+
matchTokens(searchTokenIndex, pageTokenIndex + 1, acc)
109107
// empty tokens edge cases
110108
case (Some(_), Some(_ :: _)) => matchTokens(searchTokenIndex + 1, pageTokenIndex, acc)
111109
case (Some(_ :: _), Some(_)) => matchTokens(searchTokenIndex, pageTokenIndex + 1, acc)
@@ -115,11 +113,6 @@ class PageSearchEngine(pages: List[PageEntry]):
115113
val matchedTokens = matchTokens(0, 0, Set.empty)
116114
val searchTokenPositions = searchTokens.map(_.length).scanLeft(0)(_ + _)
117115
val pageTokensPositions = pageTokens.map(_.length).scanLeft(0)(_ + _)
118-
if debuggedTypes.contains(pageName) then
119-
println("List tokens: " + matchedTokens)
120-
println("Search: " + searchTokenPositions)
121-
println("Page: " + pageTokensPositions)
122-
123116

124117
@tailrec
125118
def findHighScoreMatch(
@@ -132,39 +125,30 @@ class PageSearchEngine(pages: List[PageEntry]):
132125
consecutiveMatches: Int
133126
): Option[MatchResult] =
134127
if searchPosition >= nameSearch.length then
135-
if debuggedTypes.contains(pageName) then println("Matched " + pageName + " with score " + scoreAcc)
136128
Some(MatchResult(scoreAcc, prematched.pageEntry, positionAcc))
137129
else if pagePosition >= pageName.length then
138-
if debuggedTypes.contains(pageName) then println("Failed to match " + pageName + " :(")
139130
None
140131
else
141-
if debuggedTypes.contains(pageName) then
142-
println("At page: " + pageTokenIndex + "/" + pagePosition + "(" + pageName(pagePosition) + ")" + "; search: " + searchTokenIndex + "/" + searchPosition + "(" + nameSearch(searchPosition) + ")")
143132
val currentSearchTokenStart = searchTokenPositions(searchTokenIndex)
144-
val currentPageTokenStart = pageTokensPositions(pageTokenIndex)
145-
val atSearchTokenBeggining = searchPosition == currentSearchTokenStart
146-
val matchingPageToken = matchedTokens.find(_._1 == currentSearchTokenStart).map(_._2)
133+
val matchingPageToken = matchedTokens.find(_._1 == searchTokenIndex).map(_._2)
147134
val searchChar = nameSearch.charAt(searchPosition).toLower
148135
val pageChar = pageName.charAt(pagePosition).toLower
149136

150-
def recalculateTokenIndex(tokenPositions: Seq[Int], lastIndex: Int, position: Int): Int =
151-
if tokenPositions.length <= lastIndex + 1 || tokenPositions(lastIndex + 1) > position then
152-
lastIndex
137+
def recalculateTokenIndex(tokenPositions: Seq[Int], previousIndex: Int, position: Int): Int =
138+
if tokenPositions.length <= previousIndex + 1 || tokenPositions(previousIndex + 1) > position then
139+
previousIndex
153140
else
154-
lastIndex + 1
141+
previousIndex + 1
155142

156-
val positionScores = List(8,4,2,1).orElse(PartialFunction.fromFunction(_ => 0))
157143
def getMatchScore(matchedPagePosition: Int, matchedPageTokenStart: Int): Int =
158144
val consecutiveMatchesScore = if consecutiveMatches > 0 then 1 else 0
159145
val matchPositionScore = positionScores(matchedPagePosition - matchedPageTokenStart)
160146
val firstTokenScore = if matchPositionScore > 0 && matchedPageTokenStart == 0 then 3 else 0
161-
if debuggedTypes.contains(pageName) then println("[" + pageName + "] + score " + (consecutiveMatchesScore + matchPositionScore))
162147
consecutiveMatchesScore + matchPositionScore + firstTokenScore
163148

164149
matchingPageToken match
165150
case Some(matchingToken) if searchPosition == currentSearchTokenStart =>
166151
val matchedTokenPosition = pageTokensPositions(matchingToken)
167-
if debuggedTypes.contains(pageName) then println("Matched tokens! " + matchingToken + " at " + matchedTokenPosition)
168152
findHighScoreMatch(
169153
recalculateTokenIndex(searchTokenPositions, searchTokenIndex, searchPosition + 1),
170154
searchPosition + 1,
@@ -175,7 +159,6 @@ class PageSearchEngine(pages: List[PageEntry]):
175159
consecutiveMatches + 1
176160
)
177161
case _ if searchChar == pageChar =>
178-
if debuggedTypes.contains(pageName) then println("Matched char! " + searchChar + " at " + pagePosition)
179162
val matchedTokenPosition = matchingPageToken.map(pageTokensPositions).getOrElse(0)
180163
findHighScoreMatch(
181164
recalculateTokenIndex(searchTokenPositions, searchTokenIndex, searchPosition + 1),

scaladoc-js/main/src/searchbar/engine/QueryParser.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ class QueryParser:
1717
"type"
1818
)
1919
val kindRegex = ("(?i)" + kinds.mkString("(","|",")") + " (.*)").r
20-
val restRegex = raw"(.*)".r
20+
val nameRegex = raw"(.*)".r
2121
val escapedRegex = raw"`(.*)`".r
2222
val signatureRegex = raw"(.*=>.*)".r
2323

2424
def parseMatchers(query: String): EngineQuery = query match {
2525
case escapedRegex(rest) => NameAndKindQuery(Some(rest), None)
26-
case kindRegex(kind, rest) => NameAndKindQuery(Some(kind), Some(rest))
27-
case restRegex(name) => NameAndKindQuery(Some(name), None)
26+
case kindRegex(kind, rest) => NameAndKindQuery(Some(rest), Some(kind))
27+
case nameRegex(name) => NameAndKindQuery(Some(name), None)
2828
case _ => NameAndKindQuery(None, None)
2929
}
3030

0 commit comments

Comments
 (0)