Skip to content

Commit

Permalink
search by neurodata type
Browse files Browse the repository at this point in the history
  • Loading branch information
magland committed Feb 19, 2025
1 parent 74de5d5 commit ffa5ef7
Show file tree
Hide file tree
Showing 9 changed files with 441 additions and 74 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changes

## February 19, 2025
- Added advanced search mode for DANDI Archive Browser with neurodata type filtering
- Added matching files count indicator for advanced search results in DANDI Archive Browser
- Added horizontal spacing around splitter bar in HorizontalSplitter component
- Add SpikeDensity plugin view for NWB files
- Added job resubmission capability for failed jobs
Expand Down
193 changes: 123 additions & 70 deletions src/pages/DandiPage/DandiPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
Button,
Chip,
Stack,
FormControlLabel,
Switch,
Link,
} from "@mui/material";
import LaunchIcon from "@mui/icons-material/Launch";
Expand All @@ -21,6 +19,10 @@ import { getRecentDandisets } from "../util/recentDandisets";
import { useNavigate } from "react-router-dom";
import ScrollY from "@components/ScrollY";
import doDandiSemanticSearch from "./doDandiSemanticSearch";
import { SearchModeToggle } from "./components/SearchModeToggle";
import { AdvancedSearchPanel } from "./components/AdvancedSearchPanel";
import { useNeurodataTypesIndex } from "./hooks/useNeurodataTypesIndex";
import { doAdvancedSearch } from "./services/doAdvancedSearch";

type DandiPageProps = {
width: number;
Expand All @@ -40,30 +42,50 @@ const DandiPage: FunctionComponent<DandiPageProps> = ({ width, height }) => {
const [isSearching, setIsSearching] = useState(false);
const [lastSearchedText, setLastSearchedText] = useState("");
const [useSemanticSearch, setUseSemanticSearch] = useState(false);
const [useAdvancedSearch, setUseAdvancedSearch] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);

const {
index,
uniqueTypes,
loading: loadingTypes,
error: typesError,
} = useNeurodataTypesIndex(useAdvancedSearch);

const performSearch = useCallback(
async (searchQuery: string, limit: number) => {
setIsSearching(true);
setSearchResults([]); // Clear results before new search
const { headers, apiKeyProvided } = getDandiApiHeaders(staging);
const embargoedStr = apiKeyProvided ? "true" : "false";
const stagingStr = staging ? "-staging" : "";
const emptyStr = !searchQuery ? "false" : "true";

try {
if (useSemanticSearch) {
if (useAdvancedSearch && index) {
if (selectedTypes.length === 0) {
setTotalResults(0);
return;
}
const { results, total } = await doAdvancedSearch(
index,
selectedTypes,
limit,
);
setSearchResults(results);
setTotalResults(total);
} else if (useSemanticSearch) {
const { results, total } = await doDandiSemanticSearch(
searchQuery,
limit,
);
setSearchResults(results);
setTotalResults(total);
} else {
const { headers, apiKeyProvided } = getDandiApiHeaders(staging);
const embargoedStr = apiKeyProvided ? "true" : "false";
const stagingStr = staging ? "-staging" : "";
const emptyStr = !searchQuery ? "false" : "true";

const response = await fetch(
`https://api${stagingStr}.dandiarchive.org/api/dandisets/?page=1&page_size=50&ordering=-modified&search=${searchQuery}&draft=true&empty=${emptyStr}&embargoed=${embargoedStr}`,
{
headers,
},
{ headers },
);
if (response.status === 200) {
const json = await response.json();
Expand All @@ -78,34 +100,55 @@ const DandiPage: FunctionComponent<DandiPageProps> = ({ width, height }) => {
setLastSearchedText(searchQuery);
}
},
[staging, useSemanticSearch],
[staging, useSemanticSearch, useAdvancedSearch, index, selectedTypes],
);

// Perform initial search with empty string when component mounts
useEffect(() => {
if (searchText) return;
if (useSemanticSearch) {
if (useSemanticSearch || useAdvancedSearch) {
setSearchResults([]);
setTotalResults(0);
} else {
performSearch("", 10);
}
setRecentDandisets(getRecentDandisets());
}, [performSearch, useSemanticSearch, searchText]);
}, [performSearch, useSemanticSearch, useAdvancedSearch, searchText]);

// Reset limit when switching search modes
// Reset limit and results when switching search modes
useEffect(() => {
setCurrentLimit(10);
}, [useSemanticSearch]);
setSearchResults([]);
setTotalResults(0);
// Clear selected types when advanced search is disabled
if (!useAdvancedSearch) {
setSelectedTypes([]);
}
}, [useSemanticSearch, useAdvancedSearch]);

// Trigger search when types are selected in advanced mode
useEffect(() => {
if (useAdvancedSearch && selectedTypes.length > 0) {
performSearch(searchText, currentLimit);
}
}, [
selectedTypes,
useAdvancedSearch,
performSearch,
searchText,
currentLimit,
]);

const handleRecentClick = (dandisetId: string) => {
navigate(`/dandiset/${dandisetId}`);
};

const handleSearch = useCallback(() => {
setCurrentLimit(10);
performSearch(searchText, 10);
}, [searchText, performSearch]);
if (!useAdvancedSearch || selectedTypes.length > 0) {
setCurrentLimit(10);
performSearch(searchText, 10);
}
}, [searchText, performSearch, useAdvancedSearch, selectedTypes]);

const handleViewMore = useCallback(() => {
const newLimit = Math.min(currentLimit + 10, 50);
Expand Down Expand Up @@ -158,67 +201,77 @@ const DandiPage: FunctionComponent<DandiPageProps> = ({ width, height }) => {
</Box>
)}
<Box sx={{ mb: 1 }}>
<FormControlLabel
control={
<Switch
checked={useSemanticSearch}
onChange={(e) => setUseSemanticSearch(e.target.checked)}
/>
}
label="Use semantic search"
<SearchModeToggle
useSemanticSearch={useSemanticSearch}
onSemanticSearchChange={setUseSemanticSearch}
useAdvancedSearch={useAdvancedSearch}
onAdvancedSearchChange={setUseAdvancedSearch}
/>
</Box>
<Box sx={{ display: "flex", gap: 1, mb: 2, position: "relative" }}>
<Button
size="small"
variant="contained"
onClick={() => !isSearching && handleSearch()}
disabled={isSearching}
sx={{
minWidth: 40,
borderRadius: 2,
boxShadow: "none",
opacity: isSearching ? 0.6 : 1,
"&:hover": {
boxShadow: "none",
},
}}
>
<SearchIcon />
</Button>
<TextField
fullWidth
size="small"
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
{useAdvancedSearch && (
<Box sx={{ mb: 2 }}>
<AdvancedSearchPanel
neurodataTypes={uniqueTypes}
selectedTypes={selectedTypes}
onSelectedTypesChange={setSelectedTypes}
loading={loadingTypes}
error={typesError}
/>
</Box>
)}
{!useAdvancedSearch && (
<Box sx={{ display: "flex", gap: 1, mb: 2, position: "relative" }}>
<Button
size="small"
variant="contained"
onClick={() => !isSearching && handleSearch()}
disabled={isSearching}
sx={{
minWidth: 40,
borderRadius: 2,
backgroundColor:
searchText !== lastSearchedText
? "rgba(0, 0, 0, 0.02)"
: "transparent",
"& fieldset": {
borderColor: "rgba(0, 0, 0, 0.15)",
boxShadow: "none",
opacity: isSearching ? 0.6 : 1,
"&:hover": {
boxShadow: "none",
},
},
}}
placeholder="Search for Dandisets..."
value={searchText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(e.target.value)
}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleSearch();
}}
>
<SearchIcon />
</Button>
<TextField
fullWidth
size="small"
variant="outlined"
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: 2,
backgroundColor:
searchText !== lastSearchedText
? "rgba(0, 0, 0, 0.02)"
: "transparent",
"& fieldset": {
borderColor: "rgba(0, 0, 0, 0.15)",
},
},
}}
placeholder="Search for Dandisets..."
value={searchText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(e.target.value)
}
}}
/>
</Box>
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleSearch();
}
}}
/>
</Box>
)}
<Box>
{searchResults.map((result: DandisetSearchResultItem) => (
<DandisetSearchResult dandiset={result} key={result.identifier} />
))}
{useSemanticSearch &&
{(useSemanticSearch || useAdvancedSearch) &&
searchResults.length > 0 &&
searchResults.length < totalResults && (
<Box
Expand Down
18 changes: 14 additions & 4 deletions src/pages/DandiPage/DandisetSearchResult.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Paper, Typography, Box } from "@mui/material";
import { DandisetSearchResultItem } from "./dandi-types";
import { Paper, Typography, Box, Chip } from "@mui/material";
import { DandisetAdvancedSearchResult } from "./dandi-types";
import { useNavigate } from "react-router-dom";

const formatDate = (dateString: string) => {
Expand All @@ -9,7 +9,7 @@ const formatDate = (dateString: string) => {
import { formatBytes } from "@shared/util/formatBytes";

type Props = {
dandiset: DandisetSearchResultItem;
dandiset: DandisetAdvancedSearchResult;
};

const DandisetSearchResult = ({ dandiset }: Props) => {
Expand Down Expand Up @@ -55,7 +55,17 @@ const DandisetSearchResult = ({ dandiset }: Props) => {
{version && (
<Box sx={{ display: "flex", gap: 4 }}>
<Typography variant="body2">Version: {version.version}</Typography>
<Typography variant="body2">Files: {version.asset_count}</Typography>
<Typography variant="body2">
Files: {version.asset_count}
{dandiset.matching_files_count !== undefined && (
<Chip
size="small"
label={`${dandiset.matching_files_count} matching`}
color="primary"
sx={{ ml: 1, height: 20 }}
/>
)}
</Typography>
<Typography variant="body2">
Size: {formatBytes(version.size)}
</Typography>
Expand Down
Loading

0 comments on commit ffa5ef7

Please sign in to comment.