Skip to content

Commit b0fbc71

Browse files
improvements
1 parent 8d7babc commit b0fbc71

File tree

8 files changed

+231
-123
lines changed

8 files changed

+231
-123
lines changed

packages/web/src/app/[domain]/search/page.tsx

Lines changed: 84 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,21 @@ import { FilterPanel } from "./components/filterPanel";
2121
import { SearchResultsPanel } from "./components/searchResultsPanel";
2222
import { useDomain } from "@/hooks/useDomain";
2323
import { useToast } from "@/components/hooks/use-toast";
24-
import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
24+
import { RepositoryInfo, SearchResultFile, SearchStats } from "@/features/search/types";
2525
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
2626
import { useFilteredMatches } from "./components/filterPanel/useFilterMatches";
2727
import { Button } from "@/components/ui/button";
2828
import { ImperativePanelHandle } from "react-resizable-panels";
29-
import { FilterIcon } from "lucide-react";
29+
import { AlertTriangleIcon, FilterIcon } from "lucide-react";
3030
import { useHotkeys } from "react-hotkeys-hook";
3131
import { useLocalStorage } from "@uidotdev/usehooks";
3232
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
3333
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
3434
import { SearchBar } from "../components/searchBar";
35+
import { CodeSnippet } from "@/app/components/codeSnippet";
36+
import { CopyIconButton } from "../components/copyIconButton";
3537

36-
const DEFAULT_MAX_MATCH_COUNT = 10000;
38+
const DEFAULT_MAX_MATCH_COUNT = 500;
3739

3840
export default function SearchPage() {
3941
// We need a suspense boundary here since we are accessing query params
@@ -58,7 +60,12 @@ const SearchPageInternal = () => {
5860
const _maxMatchCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.matches) ?? `${DEFAULT_MAX_MATCH_COUNT}`);
5961
const maxMatchCount = isNaN(_maxMatchCount) ? DEFAULT_MAX_MATCH_COUNT : _maxMatchCount;
6062

61-
const { data: searchResponse, isLoading: isSearchLoading, error } = useQuery({
63+
const {
64+
data: searchResponse,
65+
isPending: isSearchPending,
66+
isFetching: isFetching,
67+
error
68+
} = useQuery({
6269
queryKey: ["search", searchQuery, maxMatchCount],
6370
queryFn: () => measure(() => unwrapServiceError(search({
6471
query: searchQuery,
@@ -68,14 +75,17 @@ const SearchPageInternal = () => {
6875
}, domain)), "client.search"),
6976
select: ({ data, durationMs }) => ({
7077
...data,
71-
durationMs,
78+
totalClientSearchDurationMs: durationMs,
7279
}),
7380
enabled: searchQuery.length > 0,
7481
refetchOnWindowFocus: false,
7582
retry: false,
76-
staleTime: Infinity,
83+
staleTime: 0,
7784
});
7885

86+
console.log(`isSearchPending`, isSearchPending);
87+
console.log(`isFetching`, isFetching);
88+
7989
useEffect(() => {
8090
if (error) {
8191
toast({
@@ -109,58 +119,31 @@ const SearchPageInternal = () => {
109119
const fileLanguages = searchResponse.files?.map(file => file.language) || [];
110120

111121
captureEvent("search_finished", {
112-
durationMs: searchResponse.durationMs,
113-
fileCount: searchResponse.zoektStats.fileCount,
114-
matchCount: searchResponse.zoektStats.matchCount,
115-
filesSkipped: searchResponse.zoektStats.filesSkipped,
116-
contentBytesLoaded: searchResponse.zoektStats.contentBytesLoaded,
117-
indexBytesLoaded: searchResponse.zoektStats.indexBytesLoaded,
118-
crashes: searchResponse.zoektStats.crashes,
119-
shardFilesConsidered: searchResponse.zoektStats.shardFilesConsidered,
120-
filesConsidered: searchResponse.zoektStats.filesConsidered,
121-
filesLoaded: searchResponse.zoektStats.filesLoaded,
122-
shardsScanned: searchResponse.zoektStats.shardsScanned,
123-
shardsSkipped: searchResponse.zoektStats.shardsSkipped,
124-
shardsSkippedFilter: searchResponse.zoektStats.shardsSkippedFilter,
125-
ngramMatches: searchResponse.zoektStats.ngramMatches,
126-
ngramLookups: searchResponse.zoektStats.ngramLookups,
127-
wait: searchResponse.zoektStats.wait,
128-
matchTreeConstruction: searchResponse.zoektStats.matchTreeConstruction,
129-
matchTreeSearch: searchResponse.zoektStats.matchTreeSearch,
130-
regexpsConsidered: searchResponse.zoektStats.regexpsConsidered,
131-
flushReason: searchResponse.zoektStats.flushReason,
122+
durationMs: searchResponse.totalClientSearchDurationMs,
123+
fileCount: searchResponse.stats.fileCount,
124+
matchCount: searchResponse.stats.totalMatchCount,
125+
actualMatchCount: searchResponse.stats.actualMatchCount,
126+
filesSkipped: searchResponse.stats.filesSkipped,
127+
contentBytesLoaded: searchResponse.stats.contentBytesLoaded,
128+
indexBytesLoaded: searchResponse.stats.indexBytesLoaded,
129+
crashes: searchResponse.stats.crashes,
130+
shardFilesConsidered: searchResponse.stats.shardFilesConsidered,
131+
filesConsidered: searchResponse.stats.filesConsidered,
132+
filesLoaded: searchResponse.stats.filesLoaded,
133+
shardsScanned: searchResponse.stats.shardsScanned,
134+
shardsSkipped: searchResponse.stats.shardsSkipped,
135+
shardsSkippedFilter: searchResponse.stats.shardsSkippedFilter,
136+
ngramMatches: searchResponse.stats.ngramMatches,
137+
ngramLookups: searchResponse.stats.ngramLookups,
138+
wait: searchResponse.stats.wait,
139+
matchTreeConstruction: searchResponse.stats.matchTreeConstruction,
140+
matchTreeSearch: searchResponse.stats.matchTreeSearch,
141+
regexpsConsidered: searchResponse.stats.regexpsConsidered,
142+
flushReason: searchResponse.stats.flushReason,
132143
fileLanguages,
133144
});
134145
}, [captureEvent, searchQuery, searchResponse]);
135146

136-
const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled, repositoryInfo, matchCount } = useMemo(() => {
137-
if (!searchResponse) {
138-
return {
139-
fileMatches: [],
140-
searchDurationMs: 0,
141-
totalMatchCount: 0,
142-
isBranchFilteringEnabled: false,
143-
repositoryInfo: {},
144-
matchCount: 0,
145-
};
146-
}
147-
148-
return {
149-
fileMatches: searchResponse.files ?? [],
150-
searchDurationMs: Math.round(searchResponse.durationMs),
151-
totalMatchCount: searchResponse.zoektStats.matchCount,
152-
isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled,
153-
repositoryInfo: searchResponse.repositoryInfo.reduce((acc, repo) => {
154-
acc[repo.id] = repo;
155-
return acc;
156-
}, {} as Record<number, RepositoryInfo>),
157-
matchCount: searchResponse.stats.matchCount,
158-
}
159-
}, [searchResponse]);
160-
161-
const isMoreResultsButtonVisible = useMemo(() => {
162-
return totalMatchCount > maxMatchCount;
163-
}, [totalMatchCount, maxMatchCount]);
164147

165148
const onLoadMoreResults = useCallback(() => {
166149
const url = createPathWithQueryParams(`/${domain}/search`,
@@ -183,20 +166,27 @@ const SearchPageInternal = () => {
183166
/>
184167
</TopBar>
185168

186-
{(isSearchLoading) ? (
169+
{(isSearchPending || isFetching) ? (
187170
<div className="flex flex-col items-center justify-center h-full gap-2">
188171
<SymbolIcon className="h-6 w-6 animate-spin" />
189172
<p className="font-semibold text-center">Searching...</p>
190173
</div>
174+
) : error ? (
175+
<div className="flex flex-col items-center justify-center h-full gap-2">
176+
<AlertTriangleIcon className="h-6 w-6" />
177+
<p className="font-semibold text-center">Failed to search</p>
178+
<p className="text-sm text-center">{error.message}</p>
179+
</div>
191180
) : (
192181
<PanelGroup
193-
fileMatches={fileMatches}
194-
isMoreResultsButtonVisible={isMoreResultsButtonVisible}
182+
fileMatches={searchResponse.files}
183+
isMoreResultsButtonVisible={searchResponse.isSearchExhaustive === false}
195184
onLoadMoreResults={onLoadMoreResults}
196-
isBranchFilteringEnabled={isBranchFilteringEnabled}
197-
repoInfo={repositoryInfo}
198-
searchDurationMs={searchDurationMs}
199-
numMatches={matchCount}
185+
isBranchFilteringEnabled={searchResponse.isBranchFilteringEnabled}
186+
repoInfo={searchResponse.repositoryInfo}
187+
searchDurationMs={searchResponse.totalClientSearchDurationMs}
188+
numMatches={searchResponse.stats.actualMatchCount}
189+
searchStats={searchResponse.stats}
200190
/>
201191
)}
202192
</div>
@@ -208,19 +198,21 @@ interface PanelGroupProps {
208198
isMoreResultsButtonVisible?: boolean;
209199
onLoadMoreResults: () => void;
210200
isBranchFilteringEnabled: boolean;
211-
repoInfo: Record<number, RepositoryInfo>;
201+
repoInfo: RepositoryInfo[];
212202
searchDurationMs: number;
213203
numMatches: number;
204+
searchStats?: SearchStats;
214205
}
215206

216207
const PanelGroup = ({
217208
fileMatches,
218209
isMoreResultsButtonVisible,
219210
onLoadMoreResults,
220211
isBranchFilteringEnabled,
221-
repoInfo,
222-
searchDurationMs,
212+
repoInfo: _repoInfo,
213+
searchDurationMs: _searchDurationMs,
223214
numMatches,
215+
searchStats,
224216
}: PanelGroupProps) => {
225217
const [previewedFile, setPreviewedFile] = useState<SearchResultFile | undefined>(undefined);
226218
const filteredFileMatches = useFilteredMatches(fileMatches);
@@ -241,6 +233,17 @@ const PanelGroup = ({
241233
description: "Toggle filter panel",
242234
});
243235

236+
const searchDurationMs = useMemo(() => {
237+
return Math.round(_searchDurationMs);
238+
}, [_searchDurationMs]);
239+
240+
const repoInfo = useMemo(() => {
241+
return _repoInfo.reduce((acc, repo) => {
242+
acc[repo.id] = repo;
243+
return acc;
244+
}, {} as Record<number, RepositoryInfo>);
245+
}, [_repoInfo]);
246+
244247
return (
245248
<ResizablePanelGroup
246249
direction="horizontal"
@@ -297,7 +300,23 @@ const PanelGroup = ({
297300
order={2}
298301
>
299302
<div className="py-1 px-2 flex flex-row items-center">
300-
<InfoCircledIcon className="w-4 h-4 mr-2" />
303+
<Tooltip>
304+
<TooltipTrigger asChild>
305+
<InfoCircledIcon className="w-4 h-4 mr-2" />
306+
</TooltipTrigger>
307+
<TooltipContent side="right" className="flex flex-col items-start gap-2">
308+
<div className="flex flex-row justify-between w-full">
309+
<p className="text-md font-medium">Search stats for nerds</p>
310+
<CopyIconButton onCopy={() => {
311+
navigator.clipboard.writeText(JSON.stringify(searchStats, null, 2));
312+
return true;
313+
}} />
314+
</div>
315+
<CodeSnippet renderNewlines>
316+
{JSON.stringify(searchStats, null, 2)}
317+
</CodeSnippet>
318+
</TooltipContent>
319+
</Tooltip>
301320
{
302321
fileMatches.length > 0 ? (
303322
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { cn } from "@/lib/utils"
22

3-
export const CodeSnippet = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
3+
export const CodeSnippet = ({ children, className, title, renderNewlines = false }: { children: React.ReactNode, className?: string, title?: string, renderNewlines?: boolean }) => {
44
return (
55
<code
66
className={cn("bg-gray-100 dark:bg-gray-700 w-fit rounded-md px-2 py-0.5 font-medium font-mono", className)}
77
title={title}
88
>
9-
{children}
9+
{renderNewlines ? <pre>{children}</pre> : children}
1010
</code>
1111
)
1212
}

packages/web/src/env.mjs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ export const env = createEnv({
1515
server: {
1616
// Zoekt
1717
ZOEKT_WEBSERVER_URL: z.string().url().default("http://localhost:6070"),
18-
SHARD_MAX_MATCH_COUNT: numberSchema.default(10000),
19-
TOTAL_MAX_MATCH_COUNT: numberSchema.default(100000),
20-
ZOEKT_MAX_WALL_TIME_MS: numberSchema.default(10000),
2118

2219
// Auth
2320
FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'),

packages/web/src/features/codeNav/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const findSearchBasedSymbolDefinitions = async (
8080
const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => {
8181
const parser = searchResponseSchema.transform(async ({ files }) => ({
8282
stats: {
83-
matchCount: searchResult.stats.matchCount,
83+
matchCount: searchResult.stats.actualMatchCount,
8484
},
8585
files: files.flatMap((file) => {
8686
const chunks = file.chunks;

packages/web/src/features/search/schemas.ts

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,35 +37,82 @@ export const repositoryInfoSchema = z.object({
3737
name: z.string(),
3838
displayName: z.string().optional(),
3939
webUrl: z.string().optional(),
40-
})
40+
});
41+
42+
// Many of these fields are defined in zoekt/api.go.
43+
export const searchStatsSchema = z.object({
44+
// The actual number of matches returned by the search.
45+
// This will always be less than or equal to `totalMatchCount`.
46+
actualMatchCount: z.number(),
47+
48+
// The total number of matches found during the search.
49+
totalMatchCount: z.number(),
50+
51+
// The duration (in nanoseconds) of the search.
52+
duration: z.number(),
53+
54+
// Number of files containing a match.
55+
fileCount: z.number(),
56+
57+
// Candidate files whose contents weren't examined because we
58+
// gathered enough matches.
59+
filesSkipped: z.number(),
60+
61+
// Amount of I/O for reading contents.
62+
contentBytesLoaded: z.number(),
63+
64+
// Amount of I/O for reading from index.
65+
indexBytesLoaded: z.number(),
66+
67+
// Number of search shards that had a crash.
68+
crashes: z.number(),
69+
70+
// Number of files in shards that we considered.
71+
shardFilesConsidered: z.number(),
72+
73+
// Files that we evaluated. Equivalent to files for which all
74+
// atom matches (including negations) evaluated to true.
75+
filesConsidered: z.number(),
76+
77+
// Files for which we loaded file content to verify substring matches
78+
filesLoaded: z.number(),
79+
80+
// Shards that we scanned to find matches.
81+
shardsScanned: z.number(),
82+
83+
// Shards that we did not process because a query was canceled.
84+
shardsSkipped: z.number(),
85+
86+
// Shards that we did not process because the query was rejected by the
87+
// ngram filter indicating it had no matches.
88+
shardsSkippedFilter: z.number(),
89+
90+
// Number of candidate matches as a result of searching ngrams.
91+
ngramMatches: z.number(),
92+
93+
// NgramLookups is the number of times we accessed an ngram in the index.
94+
ngramLookups: z.number(),
95+
96+
// Wall clock time for queued search.
97+
wait: z.number(),
98+
99+
// Aggregate wall clock time spent constructing and pruning the match tree.
100+
// This accounts for time such as lookups in the trigram index.
101+
matchTreeConstruction: z.number(),
102+
103+
// Aggregate wall clock time spent searching the match tree. This accounts
104+
// for the bulk of search work done looking for matches.
105+
matchTreeSearch: z.number(),
106+
107+
// Number of times regexp was called on files that we evaluated.
108+
regexpsConsidered: z.number(),
109+
110+
// FlushReason explains why results were flushed.
111+
flushReason: z.number(),
112+
});
41113

42114
export const searchResponseSchema = z.object({
43-
zoektStats: z.object({
44-
// The duration (in nanoseconds) of the search.
45-
duration: z.number(),
46-
fileCount: z.number(),
47-
matchCount: z.number(),
48-
filesSkipped: z.number(),
49-
contentBytesLoaded: z.number(),
50-
indexBytesLoaded: z.number(),
51-
crashes: z.number(),
52-
shardFilesConsidered: z.number(),
53-
filesConsidered: z.number(),
54-
filesLoaded: z.number(),
55-
shardsScanned: z.number(),
56-
shardsSkipped: z.number(),
57-
shardsSkippedFilter: z.number(),
58-
ngramMatches: z.number(),
59-
ngramLookups: z.number(),
60-
wait: z.number(),
61-
matchTreeConstruction: z.number(),
62-
matchTreeSearch: z.number(),
63-
regexpsConsidered: z.number(),
64-
flushReason: z.number(),
65-
}),
66-
stats: z.object({
67-
matchCount: z.number(),
68-
}),
115+
stats: searchStatsSchema,
69116
files: z.array(z.object({
70117
fileName: z.object({
71118
// The name of the file
@@ -92,6 +139,7 @@ export const searchResponseSchema = z.object({
92139
})),
93140
repositoryInfo: z.array(repositoryInfoSchema),
94141
isBranchFilteringEnabled: z.boolean(),
142+
isSearchExhaustive: z.boolean(),
95143
});
96144

97145
export const fileSourceRequestSchema = z.object({

0 commit comments

Comments
 (0)